[譯]Angular2新人常犯的5個錯誤

KathaleenB4 8年前發布 | 208K 次閱讀 Web框架 angularjs

看到這兒,我猜你肯定已經看過一些博客、技術大會錄像了,現在應該已經準備好踏上 angular2 這條不歸路了吧!那么上路后,哪些東西是我們需要知道的?

下面就是一些新手常見錯誤匯總,當你要開始自己的 angular2 旅程時,盡量避免吧。

注:本文中,我假設諸位已經對 angular2 的基礎知識有所了解。如果你是絕對新手,之前只聽說過,完全沒概念什么是 angular2 的,先去讀讀下面這些資料:

錯誤 #1:原生 hidden 屬性綁定數據

在 AngularJS 1 中,如果想切換DOM元素的顯示狀態,估計你會用 AngularJS 1 內置的指令如: ng-show 或者 ng-hide :

AngularJS 1 示例:

<div ng-show="showGreeting">
   Hello, there!
</div>

而 angular2 里,新的模版語法允許你將表達式綁定到DOM元素的任何原生屬性上。 這個絕對牛逼的功能帶來了無限的可能。其中一項就是綁定表達式到原生的 hidden 屬性上,和 ng-show 有點像,也是為元素設置 display: none :

angular2 的 [hidden] 示例(不推薦):

<div [hidden]="!showGreeting">
   Hello, there!
</div>

第一眼看上面的例子,似乎就是 AngularJS 1 里的 ng-show 。其實不然,她們有著 !important 的不同。

ng-show 和 ng-hide 都是通過一個叫 ng-hide 的CSS class來控制 DOM 元素的顯示狀態, ng-hide class就是簡單的把元素設置成 display: none 。這里的關鍵在于, AngularJS 1 在 ng-hide class里增加了 !important ,用來調整該class的優先級,使得它能夠覆蓋來自其他樣式對該元素 display 屬性的賦值。

再來說回本例,原生 hidden 屬性上的 display: none 樣式是由瀏覽器實現的。大多數瀏覽器是不會用 !important 來調整其優先級的。因此,通過 [hidden]="expression" 來控制元素顯示狀態就很容易意外的被其他樣式覆蓋掉。舉個例子:如果我在其他地方對這個元素寫了這樣一個樣式 display: flex ,這就比原生 hidden 屬性的優先級高( 看這里 )。

基于這個原因,我們通常使用 *ngIf 切換元素存在狀態來完成相同目標:

angular2 的 *ngIf 示例(推薦):

<div *ngIf="showGreeting">
   Hello, there!
</div>

和原生 hidden 屬性不同, angular2 中的 *ngIf 不受樣式約束。無論你寫了什么樣的CSS,她都是安全的。但還是有必要提一下, *ngIf 并不是控制元素的顯示狀態,而是直接通過從模版中增加/刪除元素該元素來達成顯示與否這一效果的。

當然你也可以通過全局的樣式給元素的 hidden 屬性增加隱藏的優先級,譬如: display: none !important ,來達到這個效果。你或許會問,既然 angular 小組都知道這些問題,那干嘛不在框架里直接給 hidden 加一個全局最高優先級的隱藏樣式呢?答案是我們沒法保證加全局樣式對所有應用來說都是最佳選擇。因為這種方式其實破壞了那些依賴原生 hidden 能力的功能,所以我們把選擇權交給工程師。

錯誤 #2:直接調用 DOM APIs

只有極少的情況需要直接操作 DOM 。 angular2 提供了一系列牛X的高階APIs來完成你期望的 DOM 操作,例如:queries。利用 angular2 提供的這些APIs有如下優勢:

  • 單元測試里不直接操作 DOM 可以降低測試復雜度,使你的測試用例跑的更快

  • 把你的代碼從瀏覽器中解藕,允許你在任何渲染環境里跑你的程序,譬如: web worker ,或者完全離開瀏覽器(比如:運行在服務器端,亦或是 Electron 里)

當你手動操作 DOM 時,就失去了上述優勢,而且代碼越寫越不易讀。

從 AngularJS 1 (或者壓根沒寫過 Angular 的人)轉型的朋友,我能猜到大概哪些場景是你們想直接操作 DOM 的。那我們來一起看下這些狀況,我來演示下如何用queries重構她們。

場景 一:當需要獲取當前組件模版里的某一個元素時

假設你的組件模版里有一個 input 標簽,并且你希望在組件加載后立即讓這個 input 自動獲取焦點

你或許已經知道通過 @ViewChild / @ViewChildren 這兩個queries可以獲取當前組件模版里的內嵌組件。但在這個例子里,你需要的是獲取一個普通的 HTML 元素,而非一個組件。一開始估計你就直接注入 ElementRef 來操作了:

直接操作 ElementRef (不推薦)

@Component({
  selector: 'my-comp',
  template: `
    <input type="text" />
    <div> Some other content </div>
  `
})
export class MyComp {
  constructor(el: ElementRef) {
    el.nativeElement.querySelector('input').focus();
  }
}

其實我想說的是,這種做法 沒必要

解決方案: @ViewChild 配合local template variable

程序員們沒想到的是除了組件本身,其他原生元素也是可以通過 local variable 獲取的。在寫組件時,我們可以直接在組件模版里給這個 input 標簽加標記(譬如: #myInput ), 然后把標記傳給 @ViewChild 用來獲取該元素。當組件初始化后,你就可以通過 renderer 在這個 input 標簽上執行 focus 方法了。

@ViewChild 配合 local variable (推薦)

@Component({
  selector: 'my-comp',
  template: `
    <input #myInput type="text" />
    <div> Some other content </div>
  `
})
export class MyComp implements AfterViewInit {
  @ViewChild('myInput') input: ElementRef;

  constructor(private renderer: Renderer) {}

  ngAfterViewInit() {
    this.renderer.invokeElementMethod(this.input.nativeElement,    
    'focus');
  }
}

場景 二:當需要獲取用戶映射到組件里的某個元素時

如果你想獲取的元素不在你的組件模版定義里怎么辦?舉個例子,假設你有個列表組件,允許用戶自定義各列表項,然后你想跟蹤列表項的數量。

當然你可以用 @ContentChildren 來獲取組件里的“內容”(那些用戶自定義,然后映射到你組件里的內容),但因為這些內容可以是任意值,所以是沒辦法向剛才那樣通過 local variable 來追蹤她們的。

一種方法是,要求用戶給他將要映射的列表項都加上預定義的 local variable 。這樣的話,代碼可以從上面例子改成這樣:

@ContentChildren 和 local variable (不推薦)

// user code
<my-list>
   <li *ngFor="#item of items" #list-item> {{item}} </li>
</my-list>

// component code
@Component({
  selector: 'my-list',
  template: `
    <ul>
      <ng-content></ng-content>
    </ul>
  `
})
export class MyList implements AfterContentInit {
  @ContentChildren('list-item') items: QueryList<ElementRef>;

  ngAfterContentInit() {
     // do something with list items
  }
}

可是,這需要用戶寫些額外的內容( #list-item ),真心不怎么優雅!你可能希望用戶就只寫 <li> 標簽,不要什么 #list-item 屬性,那腫么辦?

解決方案: @ContentChildren 配合 li 選擇器指令

介紹一個好方案,用 @Directive 裝飾器,配合他的 selector 功能。定義一個能查找/選擇 <li> 元素的指令,然后用 @ContentChildren 過濾用戶映射進當前組件里的內容,只留下符合條件的 li 元素。

@ContentChildren 配合 @Directive (推薦)

// user code
<my-list>
   <li *ngFor="#item of items"> {{item}} </li>
</my-list>

@Directive({ selector: 'li' })
export class ListItem {}

// component code
@Component({
  selector: 'my-list'
})
export class MyList implements AfterContentInit {
  @ContentChildren(ListItem) items: QueryList<ListItem>;

  ngAfterContentInit() {
     // do something with list items
  }
}

注:看起來只能選擇 <my-list> 里的 li 元素(例如: my-list li ),需要注意的是,目前 angular2 尚不支持"parent-child"模式的選擇器。如果需要獲取組件里的元素,用 @ViewChildren 、 @ContentChildren 這類queries是最好的選擇

錯誤 #3:在構造器里使用獲取的元素

第一次使用queries時,很容易犯這樣的錯:

在構造器里打印query的結果(錯誤)

@Component({...})
export class MyComp {
  @ViewChild(SomeDir) someDir: SomeDir;

  constructor() {
    console.log(this.someDir);// undefined
  }
}

當看到打印出來 undefined 后,你或許以為你的query壓根不能用,或者是不是構造器哪里錯了。事實上,你就是用數據用的太早了。必須要注意的是,query的結果集在組件構造時是不能用的。

幸運的是, angular2 提供了一種新的生命周期管理鉤子,可以非常輕松的幫你理清楚各類query什么時候是可用的。

  • 如果在用view query(譬如: @ViewChild , @ViewChildren )時,結果集在視圖初始化后可用。可以用 ngAfterViewInit 鉤子

  • 如果在用content query(譬如: @ContentChild , @ContentChildren )時,結果集在內容初始化后可用。可以用 ngAfterContentInit 鉤子

來動手改一下上面的例子吧:

在 ngAfterViewInit 里打印query結果集(推薦)

@Component({...})
export class MyComp implements AfterViewInit {
  @ViewChild(SomeDir) someDir: SomeDir;

  ngAfterViewInit() {
    console.log(this.someDir);// SomeDir {...}
  }
}

錯誤 #4:用 ngOnChanges 偵測query結果集的變化

在 AngularJS 1 里,如果想要監聽一個數據的變化,需要設置一個 $scope.$watch , 然后在每次digest cycle里手動判斷數據變了沒。在 angular2 里, ngOnChanges 鉤子把這個過程變得異常簡單。只要你在組件里定義了 ngOnChanges 方法,在輸入數據發生變化時該方法就會被自動調用。這超屌的!

不過需要注意的是, ngOnChanges 當且僅當組件輸入數據變化時被調用,“輸入數據”指的是通過 @Input 裝飾器顯式指定的那些數據。如果是 @ViewChildren , @ContentChildren 的結果集增加/刪除了數據, ngOnChanges 是不會被調用的。

如果你希望在query結果集變化時收到通知,那不能用 ngOnChanges 。應該通過query結果集的 changes 屬性訂閱其內置的observable。只要你在正確的鉤子里訂閱成功了(不是構造器里),當結果集變化時,你就會收到通知。

舉例,代碼應該是這個樣子的:

通過 changes 訂閱observable,監聽query結果集變化(推薦)

@Component({ selector: 'my-list' })
export class MyList implements AfterContentInit {
  @ContentChildren(ListItem) items: QueryList<ListItem>;

  ngAfterContentInit() {
    this.items.changes.subscribe(() => {
       // will be called every time an item is added/removed
    });
  }
}

如果你對observables一竅不通,趕緊的, 看這里

錯誤 #5:錯誤使用 *ngFor

在 angular2 里,我們介紹了一個新概念叫"structural directives",用來描述那些根據表達式在 DOM 上或增加、或刪除元素的指令。和其他指令不同,"structural directive"要么作用在template tag上、 要么配合template attribute使用、要么前綴"*"作為簡寫語法糖。因為這個新語法特性,初學者常常犯錯。

你能分辨出來以下錯誤么?

錯誤的 ngFor 用法

// a:
<div *ngFor="#item in items">
   <p> {{ item }} </p>
</div>

// b:
<template *ngFor #item [ngForOf]="items">
   <p> {{ item }} </p>
</template>

// c:
<div *ngFor="#item of items; trackBy=myTrackBy; #i=index">
   <p>{{i}}: {{item}} </p>
</div>

來,一步步解決錯誤

5a:把"in"換成"of"

// incorrect
<div *ngFor="#item in items">
   <p> {{ item }} </p>
</div>

如果有 AngularJS 1 經驗,通常很容易犯這個錯。在 AngularJS 1 里,相同的repeater寫作 ng-repeat="item in items" 。

angular2 將"in"換成"of"是為了和ES6中的 for-of 循環保持一致。也需要記住的是,如果不用"*"語法糖,那么完整的repeater寫法要寫作 ngForOf , 而非 ngForIn

// correct
<div *ngFor="#item of items">
   <p> {{ item }} </p>
</div>

5b:語法糖和完整語法混著寫

// incorrect
<template *ngFor #item [ngForOf]="items">
   <p> {{ item }} </p>
</template>

混著寫是沒必要的 - 而且事實上,這么寫也不工作。當你用了語法糖(前綴"*")以后, angular2 就會把她當成一個template attribute,而不是一般的指令。具體來說,解析器拿到了 ngFor 后面的字符串, 在字符串前面加上 ngFor ,然后當作template attribute來解析。如下代碼:

<div *ngFor="#item of items">

會被當成這樣:

<div template="ngFor #item of items">

當你混著寫時,他其實變成了這樣:

<template template="ngFor" #item [ngForOf]="items">

從template attribute角度分析,發現template attribute后面就只有一個 ngFor ,別的什么都沒了。那必然解析不會正確,也不會正常運行了。

如果從從template tag角度分析,他又缺了一個 ngFor 指令,所以也會報錯。沒了 ngFor 指令, ngForOf 都不知道該對誰負責了。

可以這樣修正,要么去掉"*"寫完整格式,要么就完全按照"*"語法糖簡寫方式書寫

// correct
<template ngFor #item [ngForOf]="items">
   <p> {{ item }} </p>
</template>

// correct
<p *ngFor="#item of items">
   {{ item }}
</p>

5c:在簡寫形式里用了錯誤的操作符

// incorrect
<div *ngFor="#item of items; trackBy=myTrackBy; #i=index">
   <p>{{i}}: {{item}} </p>
</div>

為了解釋這兒到底出了什么錯,我們先不用簡寫形式把代碼寫對了看看什么樣子:

// correct
<template ngFor #item [ngForOf]="items" [ngForTrackBy]="myTrackBy" #i="index">
   <p> {{i}}: {{item}} </p>
</template>

在完整形式下,結構還是很好理解的,我們來試著分解一下:

  • 我們通過輸入屬性向 ngFor 里傳入了兩組數據:

    • 綁定在 ngForOf 上的原始數據集合 items

    • 綁定在 ngForTrackBy 上的自定義track-by函數

  • 用 # 聲明了兩個 local template variables ,分別是: #i 和 #item 。 ngFor 指令在遍歷 items 時,給這兩個變量賦值

    • i 是從0開始的 items 每個元素的下標

    • item 是對應每個下標的元素

當我們通過"*"語法糖簡寫代碼時,必須遵守如下原則,以便解析器能夠理解簡寫語法:

  • 所有配置都要寫在 *ngFor 的屬性值里

  • 通過 = 操作符設置 local variable

  • 通過 : 操作符設置input properties

  • 去掉input properties里的 ngFor 前綴,譬如: ngForOf ,就只寫成 of 就可以了

  • 用分號做分隔

按照上述規范,代碼修改如下:

// correct
<p *ngFor="#item; of:items; trackBy:myTrackBy; #i=index">
   {{i}}: {{item}}
</p>

分號和冒號其實是可選的,解析器會忽略它們。寫上僅僅是為了提高代碼可讀性。因此,也可以再省略一點點:

// correct
<p *ngFor="#item of items; trackBy:myTrackBy; #i=index">
   {{i}}: {{item}}
</p>

結論

希望本章的解釋對你有用。Happy coding!

 

來自: https://segmentfault.com/a/1190000004969541

 本文由用戶 KathaleenB4 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!