看到這兒,我猜你確定已經看過一些博客、技術大會錄像了,如今應該已經準備好踏上angular2這條不歸路了吧!那麼上路後,哪些東西是咱們須要知道的?css
下面就是一些新手常見錯誤彙總,當你要開始本身的angular2
旅程時,儘可能避免吧。html
注:本文中,我假設諸位已經對angular2
的基礎知識有所瞭解。若是你是絕對新手,以前只據說過,徹底沒概念什麼是angular2
的,先去讀讀下面這些資料:程序員
Angular docsangularjs
Thoughtram's Angular 2 blogtypescript
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-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
能力的功能,因此咱們把選擇權交給工程師。
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是最好的選擇
第一次使用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 {...} } }
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一竅不通,趕忙的,看這裏
*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!