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

看到這兒,我猜你確定已經看過一些博客、技術大會錄像了,如今應該已經準備好踏上angular2這條不歸路了吧!那麼上路後,哪些東西是咱們須要知道的?css

下面就是一些新手常見錯誤彙總,當你要開始本身的angular2旅程時,儘可能避免吧。html

注:本文中,我假設諸位已經對angular2的基礎知識有所瞭解。若是你是絕對新手,以前只據說過,徹底沒概念什麼是angular2的,先去讀讀下面這些資料:程序員

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

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

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-showng-hide都是經過一個叫ng-hide的CSS class來控制DOM元素的顯示狀態,ng-hide class就是簡單的把元素設置成display: none。這裏的關鍵在於,AngularJS 1ng-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

只有極少的狀況須要直接操做DOMangular2提供了一系列牛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。這樣的話,代碼能夠從上面例子改爲這樣:

@ContentChildrenlocal 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#itemngFor指令在遍歷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!

原文地址:5 Rookie Mistakes to Avoid with Angular 2

相關文章
相關標籤/搜索