模板是編寫 Angular 組件最重要的一環(huán),你必須深入理解以下知識點(diǎn)才能玩轉(zhuǎn) Angular 模板:
對比各種 JS 模板引擎的設(shè)計(jì)思路
Mustache(八字胡)語法
模板內(nèi)的局部變量
屬性綁定似将、事件綁定、雙向綁定
在模板里面使用結(jié)構(gòu)型指令 *ngIf颤介、*ngFor、ngSwitch
在模板里面使用屬性型指令 NgClass、NgStyle、NgModel
在模板里面使用管道格式化數(shù)據(jù)
一些小 feature:安全導(dǎo)航顽爹、非空斷言
“深入理解”的含義是:你需要很自如地運(yùn)用這些 API纤泵,寫代碼的時(shí)候不翻閱 API 文檔骆姐。
因?yàn)楹芏嘈率种跃幋a效率不高,其中一個(gè)主要的原因就是在編碼過程中不停翻文檔捏题、查資料玻褪。
對比各種 JS 模板引擎的設(shè)計(jì)思路
幾乎每一款前端框架都會(huì)提供自己的模板語法:
在 jQuery 如日中天的時(shí)代,有 Handlebars 那種功能超強(qiáng)的模板
React 推崇 JSX 模板語法
當(dāng)然還有 Angular 提供的那種與“指令”緊密結(jié)合的模板語法
綜合來說公荧,無論是哪一種前端模板带射,大家都比較推崇“輕邏輯”(logic-less)的設(shè)計(jì)思路。
何為“輕邏輯”循狰?
簡而言之窟社,所謂“輕邏輯”就是說,你不能在模板里面編寫非常復(fù)雜的 JavaScript 表達(dá)式绪钥。比如灿里,Angular 的模板語法就有規(guī)定:
你不能在模板里面 new 對象
不能使用 =、+=程腹、-= 這類的表達(dá)式
不能用 ++匣吊、-- 運(yùn)算符
不能使用位運(yùn)算符
為什么要“輕邏輯”?
最重要的原因是怕影響運(yùn)行性能,因?yàn)槟0蹇赡軙?huì)被執(zhí)行很多次色鸳。
比如你編寫了以下 Angular 模板:
<ul>
? ? <li *ngFor="let race of races">
? ? ? ? {{race.name}}
? ? </li>
</ul>
很明顯社痛,瀏覽器不認(rèn)識 *ngFor 和 {{…}} 這種語法,因此必須在瀏覽器里面進(jìn)行“編譯”命雀,獲得對應(yīng)的模板函數(shù)蒜哀,然后再把數(shù)據(jù)傳遞給模板函數(shù),最終結(jié)合起來獲得一堆 HTML 標(biāo)簽吏砂,然后才能把這一堆標(biāo)簽插入到 DOM 樹里面去凡怎。
如果啟用了 AOT,處理的步驟有一些變化赊抖,@angular/cli 會(huì)對模板進(jìn)行“靜態(tài)編譯”统倒,避免在瀏覽器里面動(dòng)態(tài)編譯的過程。
而 Handlebars 這種模板引擎完全是運(yùn)行時(shí)編譯模板字符串的氛雪,你可以編寫以下代碼:
//定義模板字符串
var source=`
<ul>
? ? {{#each races}}
? ? ? ? <li>{{name}}</li>
? ? {{/each}}
</ul>
`;
//在運(yùn)行時(shí)把模板字符串編譯成 JS 函數(shù)
var templateFn=Handlebars.compile(source);
//把數(shù)據(jù)傳給模板函數(shù)房匆,獲得最終的 HTML
var html=templateFn([
? ? {name:'人族'},
? ? {name:'神族'},
? ? {name:'蟲族'}
]);
注意到 Handlebars.compile 這個(gè)調(diào)用了吧?這個(gè)地方的本質(zhì)是在運(yùn)行時(shí)把模板字符串“編譯”成了一個(gè) JS 函數(shù)报亩。
鑒于 JS 解釋執(zhí)行的特性浴鸿,你可能會(huì)擔(dān)憂這里會(huì)有性能問題。這種擔(dān)憂是合理的弦追,但是 Handlebars 是一款非常優(yōu)秀的模板引擎岳链,它在內(nèi)部做了各種優(yōu)化和緩存處理。模板字符串一般只會(huì)在第一次被調(diào)用的時(shí)候編譯一次劲件,Handlebars 會(huì)把編譯好的函數(shù)緩存起來掸哑,后面再次調(diào)用的時(shí)候會(huì)從緩存里面獲取,而不會(huì)多次進(jìn)行“編譯”零远。
上面我們多次提到了“編譯”這個(gè)詞苗分,因此很顯然這里有一個(gè)東西是無法避免的,那就是我們必須提供一個(gè) JS 版的“編譯器”牵辣,讓這個(gè)“編譯器”運(yùn)行在瀏覽器里面摔癣,這樣才能在運(yùn)行時(shí)把用戶編寫的模板字符串“編譯”成模板函數(shù)。
有一些模板引擎會(huì)真的去用 JS 編寫一款“編譯器”出來纬向,比如 Angular 和 Handlebars择浊,它們都真的編寫了一款 JS(TS)版的編譯器。而有一些簡單的模板引擎逾条,例如 Underscore 里面的模板函數(shù)琢岩,只是用正則表達(dá)式做了字符串替換而已,顯得特別簡陋膳帕。這種簡陋的模板引擎對模板的寫法有非常多的限制粘捎,因?yàn)樗皇钦嬲木幾g器薇缅,能支持的語法特性非常有限。
因此攒磨,評估一款模板引擎的強(qiáng)弱泳桦,最核心的東西就是評估它的“編譯器”做得怎么樣。但是不管怎么說娩缰,畢竟是 JS 版的“編譯器”灸撰,我們不可能把它做得像 G++ 那么強(qiáng)大,也沒有必要做得那么強(qiáng)大拼坎,因?yàn)檫@個(gè) JS 版的編譯器需要在瀏覽器里面運(yùn)行浮毯,搞得太復(fù)雜瀏覽器拖不動(dòng)!
以上就是為什么大多數(shù)模板引擎都要強(qiáng)調(diào)“輕邏輯”的最根本原因泰鸡。
對于 Angular 來說债蓝,強(qiáng)調(diào)“輕邏輯”還有另一個(gè)原因:在組件的整個(gè)生命周期里面,模板函數(shù)會(huì)被執(zhí)行很多次盛龄。你可以想象饰迹,Angular 每次要刷新組件外觀的時(shí)候,都需要去調(diào)用一下模板函數(shù)余舶,如果你在模板里面編寫了非常復(fù)雜的代碼啊鸭,一定會(huì)增加渲染時(shí)間,用戶一定會(huì)感到界面有“卡頓”匿值。
人眼的視覺延遲大約是 100ms 到 400ms 之間赠制,如果整個(gè)頁面的渲染時(shí)間超過 400ms,界面基本上就卡得沒法用了挟憔。有一些做游戲的開發(fā)者會(huì)追求 60fps 刷新率的細(xì)膩感覺钟些,60 分之 1 秒約等于 16.7ms,如果 UI 整體的渲染時(shí)間超過了 16.7ms曲楚,就沒法達(dá)到這個(gè)要求了厘唾。
輕邏輯(logic-less)帶來了效率的提升褥符,也帶來了一些不方便龙誊,比如很多模板引擎都實(shí)現(xiàn)了 if 語句,但是沒有實(shí)現(xiàn) else喷楣,因此開發(fā)者們在編寫復(fù)雜業(yè)務(wù)邏輯的時(shí)候模板代碼會(huì)顯得非常啰嗦趟大。
目前來說,并沒有完美的方案能同時(shí)兼顧運(yùn)行效率和語法表現(xiàn)能力铣焊,這里只能取一個(gè)平衡逊朽。
Mustache 語法
Mustache 語法也就是你們說的雙花括號語法 {{…}},老外覺得它像八字胡子曲伊,很奇怪啊叽讳,難道老外喜歡側(cè)著頭看東西追他?
好消息是,很多模板引擎都接受了 Mustache 語法岛蚤,這樣一來學(xué)習(xí)量又降低了不少邑狸,開心吧?
關(guān)于 Mustache 語法谤逼,你需要掌握 3 點(diǎn):
它可以獲取到組件里面定義的屬性值
它可以自動(dòng)計(jì)算簡單的數(shù)學(xué)表達(dá)式瑰剃,如加減乘除羡棵、取模
它可以獲得方法的返回值
請依次看例子。
插值語法關(guān)鍵代碼實(shí)例:
<h3>
? ? 歡迎來到{{title}}硅堆!
</h3>
public title = '假的星際爭霸2';
簡單的數(shù)學(xué)表達(dá)式求值:
<h3>1+1={{1+1}}</h3>
調(diào)用組件里面定義的方法:
<h3>可以調(diào)用方法{{getVal()}}</h3>
public getVal():any{
? ? return 65535;
}
模板內(nèi)的局部變量
<input #heroInput>
<p>{{heroInput.value}}</p>
有一些朋友會(huì)追問,如果我在模板里面定義的局部變量和組件內(nèi)部的屬性重名會(huì)怎么樣呢贿讹?
如果真的出現(xiàn)了重名渐逃,Angular 會(huì)按照以下優(yōu)先級來進(jìn)行處理:
模板局部變量 > 指令中的同名變量 > 組件中的同名屬性。
復(fù)制
這種優(yōu)先級規(guī)則和 JSP 里面的變量取值規(guī)則非常類似民褂,對比一下很好理解對不對朴乖?你可以自己寫代碼測試一下。
值綁定
值綁定是用方括號來做的助赞,寫法:
<img [src]="imgSrc" />
public imgSrc:string="./assets/imgs/1.jpg";
很明顯买羞,這種綁定是單向的。
事件綁定
事件綁定是用圓括號來做的雹食,寫法:
<button class="btn btn-success" (click)="btnClick($event)">測試事件</button>
對應(yīng) Component 內(nèi)部的方法定義:
public btnClick(event):void{
? ? alert("測試事件綁定畜普!");
}
雙向綁定
雙向綁定是通過方括號里面套一個(gè)圓括號來做的,模板寫法:
<font-resizer [(size)]="fontSizePx"></font-resizer>
對應(yīng)組件內(nèi)部的屬性定義:
public fontSizePx:number=14;
AngularJS 是第一個(gè)把“雙向數(shù)據(jù)綁定”這個(gè)特性帶到前端來的框架群叶,這也是 AngularJS 當(dāng)年最受開發(fā)者追捧的特性吃挑,之一。
根據(jù) AngularJS 團(tuán)隊(duì)當(dāng)年講的故事街立,“雙向數(shù)據(jù)綁定”這個(gè)特性可以大幅度壓縮前端代碼的規(guī)模舶衬。大家可以回想一下 jQuery 時(shí)代的做法,如果要實(shí)現(xiàn)類似的效果赎离,是不是要自己去編寫大量的代碼逛犹?尤其是那種大規(guī)模的表單,一大堆的賦值和取值操作梁剔,都是非常丑陋的“面條”代碼虽画,而有了“雙向數(shù)據(jù)綁定”特性之后,一個(gè)綁定表達(dá)式就搞定荣病。
目前码撰,主流的幾款前端框架都已經(jīng)接受了“雙向數(shù)據(jù)綁定”這個(gè)特性。
當(dāng)然个盆,也有一些人不喜歡“雙向數(shù)據(jù)綁定”脖岛,還有人專門寫了文章來進(jìn)行批判朵栖,也算是前端一景。
在模板里面使用結(jié)構(gòu)型指令
Angular 有 3 個(gè)內(nèi)置的結(jié)構(gòu)型指令:*ngIf柴梆、*ngFor混槐、ngSwitch。ngSwitch 的語法比較啰嗦轩性,使用頻率小一些声登。
*ngIf 代碼實(shí)例:
<p *ngIf="isShow" style="background-color:#ff3300">顯示還是不顯示?</p>
<button class="btn btn-success" (click)="toggleShow()">控制顯示隱藏</button>
public isShow:boolean=true;
public toggleShow():void{
? ? this.isShow=!this.isShow;
}
*ngFor 代碼實(shí)例:
<li *ngFor="let race of races;let i=index;">
? ? {{i+1}}-{{race.name}}
</li>
public races:Array=[
? ? {name:"人族"},
? ? {name:"蟲族"},
? ? {name:"神族"}
];
*ngSwitch 代碼實(shí)例:
<div [ngSwitch]="mapStatus">
? ? <p *ngSwitchCase="0">下載中...</p>
? ? <p *ngSwitchCase="1">正在讀取...</p>
? ? <p *ngSwitchDefault>系統(tǒng)繁忙...</p>
</div>
public mapStatus:number=1;
特別注意:一個(gè) HTML 標(biāo)簽上只能同時(shí)使用一個(gè)結(jié)構(gòu)型的指令揣苏。
因?yàn)椤敖Y(jié)構(gòu)型”指令會(huì)修改 DOM 結(jié)構(gòu)悯嗓,如果在一個(gè)標(biāo)簽上使用多個(gè)結(jié)構(gòu)型指令,大家都一起去修改 DOM 結(jié)構(gòu)卸察,到時(shí)候到底誰說了算脯厨?
那么需要在同一個(gè) HTML 上使用多個(gè)結(jié)構(gòu)型指令應(yīng)該怎么辦呢?有兩個(gè)辦法:
加一層空的 div 標(biāo)簽
加一層 <ng-container>
在模板里面使用屬性型指令
使用頻率比較高的 3 個(gè)內(nèi)置指令是:NgClass坑质、NgStyle合武、NgModel。
NgClass 使用案例代碼:
<div [ngClass]="currentClasses">同時(shí)批量設(shè)置多個(gè)樣式</div>
<button class="btn btn-success" (click)="setCurrentClasses()">設(shè)置</button>
public currentClasses: {};
public canSave: boolean = true;
public isUnchanged: boolean = true;
public isSpecial: boolean = true;
setCurrentClasses() {
? ? this.currentClasses = {
? ? ? ? 'saveable': this.canSave,
? ? ? ? 'modified': this.isUnchanged,
? ? ? ? 'special': this.isSpecial
? ? };
}
.saveable{
? ? font-size: 18px;
}
.modified {
? ? font-weight: bold;
}
.special{
? ? background-color: #ff3300;
}
NgStyle 使用案例代碼:
<div [ngStyle]="currentStyles">
? ? 用NgStyle批量修改內(nèi)聯(lián)樣式涡扼!
</div>
<button class="btn btn-success" (click)="setCurrentStyles()">設(shè)置</button>
public currentStyles: {}
public canSave:boolean=false;
public isUnchanged:boolean=false;
public isSpecial:boolean=false;
setCurrentStyles() {
? ? this.currentStyles = {
? ? ? ? 'font-style':? this.canSave? ? ? ? 'italic' : 'normal',
? ? ? ? 'font-weight': !this.isUnchanged ? 'bold'? : 'normal',
? ? ? ? 'font-size':? this.isSpecial? ? ? '36px'? : '12px'
? ? };
}
ngStyle 這種方式相當(dāng)于在代碼里面寫 CSS 樣式稼跳,比較丑陋,違反了注意點(diǎn)分離的原則吃沪,而且將來不太好修改汤善,非常不建議這樣寫。
NgModel 使用案例代碼:
<p class="text-danger">ngModel只能用在表單類的元素上面</p>
? ? <input [(ngModel)]="currentRace.name">
<p>{{currentRace.name}}</p>
public currentRace:any={name:"隨機(jī)種族"};
請注意票彪,如果你需要使用 NgModel 來進(jìn)行雙向數(shù)據(jù)綁定红淡,必須要在對應(yīng)的模塊里面 import FormsModule 。
管道
管道的一個(gè)典型作用是用來格式化數(shù)據(jù)降铸,來一個(gè)最簡單的例子:
{{currentTime | date:'yyyy-MM-dd HH:mm:ss'}}
public currentTime: Date = new Date();
Angular 里面一共內(nèi)置了 17 個(gè)指令(有一些已經(jīng)過時(shí)了):
在復(fù)雜的業(yè)務(wù)場景里面在旱,17 個(gè)指令肯定不夠用,如果需要自定義指令推掸,請查看這里的例子: https://angular.io/guide/pipes 桶蝎。