1. 程式人生 > >Web前端學習筆記——AngularJS之元件、模板語法

Web前端學習筆記——AngularJS之元件、模板語法

目錄

元件

元件的定義

元件的模板

元件通訊

小結

動態元件

模板語法

插值

文字繫結

屬性繫結

列表渲染

條件渲染

NgIf

事件處理

文字

多行文字

複選框

Class

Style

過濾器

總結

元件

ng-component

幾乎所有前端框架都在玩“元件化”,而且最近都不約而同地選擇了“標籤化”這種思路,Angular 也不例外。

對新版本的 Angular 來說,一切都是圍繞著“元件化”展開的,元件是 Angular 的核心概念模型。

Component Tree

元件的定義

以下是一個最簡單的 Angular 元件定義:

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'itcast';
}
  • @Component:這是一個 Decorator(裝飾器),其作用類似於 Java 裡面的註解。Decorator 這個語言特性目前(2017-10)處於 Stage 2(草稿)狀態,還不是 ECMA 的正式規範。
  • selector:元件的標籤名,外部使用者可以這樣來使用這個元件:。預設情況下,ng 命令生成出來的元件都會帶上一個 app 字首,如果你不喜歡,可以在 angular-cli.json 裡面修改 prefix 配置項,設定為空字串將會不帶任何字首。
  • templateUrl:引用外部的 HTML 模板。如果你想直接編寫內聯模板,可以使用 template,支援 ES6 引入的“模板字串”寫法
  • styleUrls:引用外部 CSS 樣式檔案,這是一個數組,也就意味著可以引用多份 CSS 檔案。
  • export class AppComponent:這是 ES6 裡面引入的模組和 class 定義方式。

元件的模板

  • 內聯模板
  • 模板檔案

你可以在兩種地方存放元件模板。 你可以使用template屬性把它定義為內聯的,或者把模板定義在一個獨立的 HTML 檔案中, 再通過@Component裝飾器中的templateUrl屬性, 在元件元資料中把它連結到元件。

無論用哪種風格,模板資料繫結在訪問元件屬性方面都是完全一樣的。

具體模板語法請參考:模板語法

元件通訊

元件就像零散的積木,我們需要把這些積木按照一定的規則拼裝起來,而且要讓它們互相之間能進行通訊,這樣才能構成一個有機的完整系統。

在真實的應用中,元件最終會構成樹形結構,就像人類社會中的家族樹一樣:

891636a0-af23-11e7-b111-4d6e630f480d.png

在樹形結構裡面,元件之間有幾種典型的關係:父子關係、兄弟關係、沒有直接關係。

相應地,元件之間有以下幾種典型的通訊方案:

  • 直接的父子關係:父元件直接訪問子元件的 public 屬性和方法。
  • 直接的父子關係:藉助於 @Input 和 @Output 進行通訊
  • 沒有直接關係:藉助於 Service 單例進行通訊。
  • 利用 cookie 和 localstorage 進行通訊。
  • 利用 session 進行通訊。

無論你使用什麼前端框架,元件之間的通訊都離開不以上幾種方案,這些方案與具體框架無關。

  1. 父元件通過子元件標籤傳遞屬性
  2. 子元件在內部宣告 @Input 接收

  3. Input 是單向的

    • 父元件如果把資料改了,子元件也會更新
    • 但是反之不會
    • 有一個例外,引用型別修改

下面是一個示例:

子元件:

import { Component, Input } from '@angular/core';

import { Hero } from './hero';

@Component({
  selector: 'app-hero-child',
  template: `
    <h3>{{hero.name}} says:</h3>
    <p>I, {{hero.name}}, am at your service, {{masterName}}.</p>
  `
})
export class HeroChildComponent {
  // 宣告接收父元件傳遞的資料
  @Input() hero: Hero;
  @Input('master') masterName: string; // 接收 master 重新命名為 masterName
}

父元件:

import { Component } from '@angular/core';

import { HEROES } from './hero';

@Component({
  selector: 'app-hero-parent',
  template: `
    <h2>{{master}} controls {{heroes.length}} heroes</h2>
    <!-- 在子元件標籤上傳遞資料 -->
    <app-hero-child *ngFor="let hero of heroes"
      [hero]="hero"
      [master]="master">
    </app-hero-child>
  `
})
export class HeroParentComponent {
  heroes = HEROES;
  master = 'Master';
}

@Output 的本質是事件機制,我們可以利用它來訂閱子元件上釋出的事件,子元件上這樣寫:

import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'app-voter',
  template: `
    <h4>{{name}}</h4>
    <button (click)="vote(true)"  [disabled]="voted">Agree</button>
    <button (click)="vote(false)" [disabled]="voted">Disagree</button>
  `
})
export class VoterComponent {
  @Input()  name: string;
  @Output() onVoted = new EventEmitter<boolean>();
  voted = false;

  vote(agreed: boolean) {
    this.onVoted.emit(agreed); // 傳遞的資料就是事件物件
    this.voted = true;
  }
}

在父元件中訂閱處理:

import { Component }      from '@angular/core';

@Component({
  selector: 'app-vote-taker',
  template: `
    <h2>Should mankind colonize the Universe?</h2>
    <h3>Agree: {{agreed}}, Disagree: {{disagreed}}</h3>
    <app-voter *ngFor="let voter of voters"
      [name]="voter"
      (onVoted)="onVoted($event)">
    </app-voter>
    <!-- $event在這裡是自定義事件物件,接收到的是子元件內部發布事件傳遞的資料 -->
  `
})
export class VoteTakerComponent {
  agreed = 0;
  disagreed = 0;
  voters = ['Mr. IQ', 'Ms. Universe', 'Bombasto'];

  onVoted(agreed: boolean) {
    agreed ? this.agreed++ : this.disagreed++;
  }
}

對於有直接父子關係的元件,父元件可以直接訪問子元件裡面 public 型的屬性和方法,示例程式碼片段如下:

<app-foo #child></app-foo>
<button (click)="child.increment()">呼叫子元件的方法</button>

顯然,子元件裡面必須暴露一個 public 型的 childFn 方法,就像這樣:

export class FooComponent implements OnInit {
  public message: string = 'foo message'
  public count: number = 0

  constructor() { }

  public increment (): void {
    this.count++
  }

  ngOnInit() {
  }
}

以上是通過在模板裡面定義區域性變數的方式來直接呼叫子元件裡面的 public 型方法。在父元件的內部也可以訪問到子元件的例項,需要利用到 @ViewChild 裝飾器,示例如下:

@ViewChild(ChildComponent)
private childComponent: ChildComponent;

關於 @ViewChild 在後面的內容裡面會有更詳細的解釋。

很明顯,如果父元件直接訪問子元件,那麼兩個元件之間的關係就被固定死了。父子兩個元件緊密依賴,誰也離不開誰,也就都不能單獨使用了。所以,除非你知道自己在做什麼,最好不要直接在父元件裡面直接訪問子元件上的屬性和方法,以免未來一改一大片。

d2615600-af23-11e7-9203-4582e2e80f6b.png

e9aaf1e0-af23-11e7-b111-4d6e630f480d.png

79246040-af24-11e7-b111-4d6e630f480d.png

小結

元件間的通訊方案是通用的,無論你使用什麼樣的前端框架,都會面臨這個問題,而解決的方案無外乎本文所列出的幾種。

hooks-in-sequence.png

  • ngOnChanges()
  • ngOnInit()
    • 只執行一次
  • ngDoCheck()
  • ngAfterContentInit()
    • 只執行一次
  • ngAfterContentChecked()
  • ngAfterViewInit()
    • 只執行一次
  • ngAfterViewChecked()
  • ngOnDestroy()
    • 只執行一次
  • Angular 一共暴露了8個“鉤子”,建構函式不算。
  • 並沒有元件或者指令會實現全部鉤子。
  • 綠色的4個鉤子可能會被執行很多次,紫色的只會執行一次。
  • Content 和 View 相關的4個鉤子只對元件有效,指令上不能使用。因為在新版本的 Angular 裡面,指令不能帶有 HTML 模板。指令沒有自己的 UI,當然就沒有 View 和 Content 相關的“鉤子”了。
  • 請不要在生命週期鉤子裡面實現複雜的業務邏輯,尤其是那4個會被反覆執行的鉤子,否則一定會造成介面卡頓。

動態元件

注意:用程式碼動態建立元件這種方式在一般的業務開發裡面不常用,而且可能存在一些隱藏的坑,如果你一定要用,請小心避雷。

模板語法

1200px-TempEngGen015.svg.png

插值

文字繫結

<p>Message: {{ msg }}</p>

<p [innerHTML]="msg"></p>

屬性繫結

<!-- 寫法一 -->
<img src="{{ heroImageUrl }}">

<!-- 寫法二,推薦 -->
<img [src]="heroImageUrl">

<!-- 寫法三 -->
<img bind-src="heroImageUrl">

在布林特性的情況下,它們的存在即暗示為 true,屬性繫結工作起來略有不同,在這個例子中:

<button [disabled]="isButtonDisabled">Button</button>

如果 isButtonDisabled 的值是 nullundefined 或 false,則 disabled 特性甚至不會被包含在渲染出來的 <button> 元素中。

<p>1 + 1 = {{ 1 + 1 }}</p>
<p>{{ num + 1 }}</p>
<p>{{ isDone ? '完了' : '沒完' }}</p>
<p>{{ title.split('').reverse().join('') }}</p>

<p [title]="title.split('').reverse().join('')">{{ title }}</p>

<ul>
  <li [id]="'list-' + t.id" *ngFor="let t of todos">
    {{ t.title }}
  </li>
</ul>

編寫模板表示式所用的語言看起來很像 JavaScript。 很多 JavaScript 表示式也是合法的模板表示式,但不是全部。

Angular 遵循輕邏輯的設計思路,所以在模板引擎中不能編寫非常複雜的 JavaScript 表示式,這裡有一些約定:

  • 賦值 (=+=-=, ...)
  • new 運算子
  • 使用 ; 或 , 的鏈式表示式
  • 自增或自減操作符 (++--)

列表渲染

基本用法:

export class AppComponent {
  heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado'];
}
<p>Heroes:</p>
<ul>
  <li *ngFor="let hero of heroes">
    {{ hero }}
  </li>
</ul>

也可以獲取 index 索引:

<div *ngFor="let hero of heroes; let i=index">{{i + 1}} - {{hero.name}}</div>

條件渲染

NgIf

<p *ngIf="heroes.length > 3">There are many heroes!</p>
<ng-template [ngIf]="condition"><div>...</div></ng-template>

NgSwitch 的語法比較囉嗦,使用頻率小一些。

<div [ngSwitch]="currentHero.emotion">
  <app-happy-hero    *ngSwitchCase="'happy'"    [hero]="currentHero"></app-happy-hero>
  <app-sad-hero      *ngSwitchCase="'sad'"      [hero]="currentHero"></app-sad-hero>
  <app-confused-hero *ngSwitchCase="'confused'" [hero]="currentHero"></app-confused-hero>
  <app-unknown-hero  *ngSwitchDefault           [hero]="currentHero"></app-unknown-hero>
</div>

ngswitch

事件處理

事件繫結只需要用圓括號把事件名包起來即可:

<button (click)="onSave()">Save</button>

可以把事件物件傳遞到事件處理函式中:

<button (click)="onSave($event)">On Save</button>

也可以傳遞額外的引數(取決於你的業務):

<button (click)="onSave($event, 123)">On Save</button>

當事件處理語句比較簡單的時候,我們可以內聯事件處理語句:

<button (click)="message = '哈哈哈'">內聯事件處理</button>

我們可以利用 屬性繫結 + 事件處理 的方式實現表單文字框雙向繫結:

<input [value]="message"
       (input)="message=$event.target.value" >

事件繫結的另一種寫法,使用 on- 字首的方式:

<!-- 繫結事件處理函式 -->
<button on-click="onSave()">On Save</button>

文字

<p>{{ message }}</p>
<input type="text" [(ngModel)]="message">

執行之後你會發現報錯了,原因是使用 ngModel 必須匯入 FormsModule 並把它新增到 Angular 模組的 imports列表中。

匯入 FormsModule 並讓 [(ngModel)] 可用的程式碼如下:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
+++ import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
+++ FormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

通過以上的配置之後,你就可以開心的在 Angular 中使用雙向資料綁定了