Angular 應(yīng)用主要由組件及其 HTML 模板組成。由于瀏覽器無法直接理解 Angular 所提供的組件和模板,因此 Angular 應(yīng)用程序需要先進(jìn)行編譯才能在瀏覽器中運(yùn)行。
在瀏覽器下載和運(yùn)行代碼之前的編譯階段,Angular 預(yù)先(AOT)編譯器會(huì)先把你的 Angular HTML 和 TypeScript 代碼轉(zhuǎn)換成高效的 JavaScript 代碼。在構(gòu)建期間編譯應(yīng)用可以讓瀏覽器中的渲染更快速。
本指南中解釋了如何指定元數(shù)據(jù),并使用一些編譯器選項(xiàng)以借助 AOT 編譯器來更有效的編譯應(yīng)用。
下面是你可能要使用 AOT 的部分原因。
原因 |
詳情 |
---|---|
更快的渲染方式 |
使用 AOT,瀏覽器會(huì)下載應(yīng)用程序的預(yù)編譯版本。瀏覽器加載可執(zhí)行代碼,以便立即渲染應(yīng)用程序,而無需等待先編譯應(yīng)用程序。 |
更少的異步請求 |
編譯器在應(yīng)用程序 JavaScript 中內(nèi)聯(lián)外部 HTML 模板和 CSS 樣式表,消除對這些源文件的單個(gè) ajax 請求。 |
更小的 Angular 框架下載大小 |
如果應(yīng)用程序已被編譯,則無需下載 Angular 編譯器。編譯器大約是 Angular 本身的一半,因此省略它會(huì)大大減少應(yīng)用程序的體積。 |
及早檢測模板錯(cuò)誤 |
AOT 編譯器會(huì)在用戶看到之前在構(gòu)建步驟中檢測并報(bào)告模板綁定錯(cuò)誤。 |
更好的安全性 |
AOT 會(huì)在 HTML 模板和組件提供給客戶端之前就將它們編譯為 JavaScript 文件。由于沒有要讀取的模板,也沒有危險(xiǎn)的客戶端 HTML 或 JavaScript 求值,因此注入攻擊的機(jī)會(huì)更少。 |
Angular 提供了兩種方式來編譯你的應(yīng)用:
ANGULAR 編譯方式 |
詳情 |
---|---|
即時(shí) (JIT) |
當(dāng)運(yùn)行時(shí)在瀏覽器中編譯你的應(yīng)用程序。在 Angular 8 之前,這是默認(rèn)值。 |
預(yù)先 (AOT) |
在構(gòu)建時(shí)編譯你的應(yīng)用程序和庫。這是從 Angular 9 開始的默認(rèn)值。 |
當(dāng)運(yùn)行 CLI 命令 ?ng build
? (只構(gòu)建) 或 ?ng serve
? (構(gòu)建并啟動(dòng)本地服務(wù)器) 時(shí),編譯類型(JIT 或 AOT)取決于你在 ?angular.json
? 中的構(gòu)建配置所指定的 ?aot
?屬性。默認(rèn)情況下,對于新的 CLI 應(yīng)用,其 ?aot
?為 ?true
?。
Angular AOT 編譯器會(huì)提取元數(shù)據(jù)來解釋應(yīng)由 Angular 管理的應(yīng)用程序部分。你可以在裝飾器(比如 ?@Component()
? 和 ?@Input()
?)中顯式指定元數(shù)據(jù),也可以在被裝飾的類的構(gòu)造函數(shù)聲明中隱式指定元數(shù)據(jù)。元數(shù)據(jù)告訴 Angular 要如何構(gòu)造應(yīng)用程序類的實(shí)例并在運(yùn)行時(shí)與它們進(jìn)行交互。
在下列范例中,?@Component()
? 元數(shù)據(jù)對象和類的構(gòu)造函數(shù)會(huì)告訴 Angular 如何創(chuàng)建和顯示 ?TypicalComponent
?的實(shí)例。
@Component({
selector: 'app-typical',
template: '<div>A typical component for {{data.name}}</div>'
})
export class TypicalComponent {
@Input() data: TypicalData;
constructor(private someService: SomeService) { … }
}
Angular 編譯器只提取一次元數(shù)據(jù),并且為 ?TypicalComponent
?生成一個(gè)工廠。當(dāng)它需要?jiǎng)?chuàng)建 ?TypicalComponent
?的實(shí)例時(shí),Angular 調(diào)用這個(gè)工廠,工廠會(huì)生成一個(gè)新的可視元素,并且把它(及其依賴)綁定到組件類的一個(gè)新實(shí)例上。
AOT 編譯分為三個(gè)階段。
階段 |
詳情 |
|
---|---|---|
1 |
代碼分析 |
在此階段,TypeScript 編譯器和AOT 收集器會(huì)創(chuàng)建源代碼的表示。收集器不會(huì)嘗試解釋它收集的元數(shù)據(jù)。它會(huì)盡可能地表示元數(shù)據(jù),并在檢測到元數(shù)據(jù)語法違規(guī)時(shí)記錄錯(cuò)誤。 |
2 |
代碼生成 |
在此階段,編譯器的 |
3 |
模板類型檢查 |
在此可選階段,Angular 模板編譯器使用 TypeScript 編譯器來驗(yàn)證模板中的綁定表達(dá)式。你可以通過設(shè)置 |
你只能使用 TypeScript 的一個(gè)子集書寫元數(shù)據(jù),它必須滿足下列限制:
關(guān)于準(zhǔn)備 AOT 編譯應(yīng)用程序的其它準(zhǔn)則和說明,參閱 Angular:編寫 AOT 友好的應(yīng)用程序。
AOT 編譯中的錯(cuò)誤通常是由于元數(shù)據(jù)不符合編譯器的要求而發(fā)生的(下面將更全面地介紹)。
你可以在 ?tsconfig.json
? TypeScript 配置文件中提供控制編譯過程的選項(xiàng)。
TypeScript 編譯器會(huì)做一些初步的分析工作,它會(huì)生成類型定義文件?.d.ts
?,其中帶有類型信息,Angular 編譯器需要借助它們來生成代碼。 同時(shí),AOT 收集器(collector) 會(huì)記錄 Angular 裝飾器中的元數(shù)據(jù),并把它們輸出到?.metadata.json
?文件中,和每個(gè) ?.d.ts
? 文件相對應(yīng)。
你可以把 ?.metadata.json
? 文件看做一個(gè)包括全部裝飾器的元數(shù)據(jù)的全景圖,就像抽象語法樹 (AST)一樣。
Angular 的 ?
schema.ts
? 會(huì)將 JSON 格式描述為 TypeScript 接口的集合。
AOT 收集器只能理解 JavaScript 的一個(gè)子集。定義元數(shù)據(jù)對象時(shí)要遵循下列語法限制:
語法 |
范例 |
---|---|
對象字面量 |
{cherry: true, apple: true, mincemeat: false}
|
數(shù)組字面量 |
['cherries', 'flour', 'sugar']
|
展開數(shù)組字面量 |
['apples', 'flour', ...]
|
函數(shù)調(diào)用 |
bake(ingredients)
|
新建對象 |
new Oven()
|
屬性訪問 |
pie.slice
|
數(shù)組索引訪問 |
ingredients[0]
|
引用標(biāo)識(shí)符 |
Component
|
模板字符串 |
`pie is ${multiplier} times better than cake`
|
字符串字面量 |
'pi'
|
數(shù)字字面量 |
3.14153265
|
邏輯字面量 |
true
|
null 字面量 |
null
|
受支持的前綴運(yùn)算符 |
!cake
|
受支持的二元運(yùn)算符 |
a+b
|
條件運(yùn)算符 |
a ? b : c
|
括號(hào) |
(a+b)
|
如果表達(dá)式使用了不支持的語法,收集器就會(huì)往 ?.metadata.json
? 文件中寫入一個(gè)錯(cuò)誤節(jié)點(diǎn)。稍后,如果編譯器用到元數(shù)據(jù)中的這部分內(nèi)容來生成應(yīng)用代碼,它就會(huì)報(bào)告這個(gè)錯(cuò)誤。
如果你希望 ?
ngc
?立即匯報(bào)這些語法錯(cuò)誤,而不要生成帶有錯(cuò)誤信息的 ?.metadata.json
? 文件,可以到 TypeScript 的配置文件中設(shè)置 ?strictMetadataEmit
?選項(xiàng)。"angularCompilerOptions": { … "strictMetadataEmit" : true }
Angular 庫通過這個(gè)選項(xiàng)來確保所有的 Angular ?
.metadata.json
? 文件都是干凈的。當(dāng)你要構(gòu)建自己的代碼庫時(shí),這也同樣是一項(xiàng)最佳實(shí)踐。
AOT 編譯器不支持函數(shù)表達(dá)式和箭頭函數(shù),也叫 lambda 函數(shù)。
考慮如下組件裝飾器:
@Component({
…
providers: [{provide: server, useFactory: () => new Server()}]
})
AOT 的收集器不支持在元數(shù)據(jù)表達(dá)式中出現(xiàn)箭頭函數(shù) ?() => new Server()
?。它會(huì)在該函數(shù)中就地生成一個(gè)錯(cuò)誤節(jié)點(diǎn)。稍后,當(dāng)編譯器解釋該節(jié)點(diǎn)時(shí),它就會(huì)報(bào)告一個(gè)錯(cuò)誤,讓你把這個(gè)箭頭函數(shù)轉(zhuǎn)換成一個(gè)導(dǎo)出的函數(shù)。
你可以把它改寫成這樣來修復(fù)這個(gè)錯(cuò)誤:
export function serverFactory() {
return new Server();
}
@Component({
…
providers: [{provide: server, useFactory: serverFactory}]
})
在版本 5 和更高版本中,編譯器會(huì)在發(fā)出 ?.js
? 文件時(shí)自動(dòng)執(zhí)行此重寫。
編譯器只會(huì)解析到已導(dǎo)出符號(hào)的引用。收集器可以在收集期間執(zhí)行表達(dá)式,并用其結(jié)果記錄到 ?.metadata.json
? 中(而不是原始表達(dá)式中)。這樣可以讓你把非導(dǎo)出符號(hào)的使用限制在表達(dá)式中。
比如,收集器可以估算表達(dá)式 ?1 + 2 + 3 + 4
? 并將其替換為結(jié)果 ?10
?。這個(gè)過程稱為?折疊
?。可以用這種方式簡化的表達(dá)式是可折疊的。
收集器可以計(jì)算對模塊局部變量的 ?const
?聲明和初始化過的 ?var
?和 ?let
?聲明,并從 ?.metadata.json
? 文件中移除它們。
考慮下列組件定義:
const template = '<div>{{hero.name}}</div>';
@Component({
selector: 'app-hero',
template: template
})
export class HeroComponent {
@Input() hero: Hero;
}
編譯器不能引用 ?template
?常量,因?yàn)樗俏磳?dǎo)出的。但是收集器可以通過內(nèi)聯(lián) ?template
?常量的方式把它折疊進(jìn)元數(shù)據(jù)定義中。最終的結(jié)果和你以前的寫法是一樣的:
@Component({
selector: 'app-hero',
template: '<div>{{hero.name}}</div>'
})
export class HeroComponent {
@Input() hero: Hero;
}
這里沒有對 ?template
?的引用,因此,當(dāng)編譯器稍后對位于 ?.metadata.json
? 中的收集器輸出進(jìn)行解釋時(shí),不會(huì)再出問題。
你還可以通過把 ?template
?常量包含在其它表達(dá)式中來讓這個(gè)例子深入一點(diǎn):
const template = '<div>{{hero.name}}</div>';
@Component({
selector: 'app-hero',
template: template + '<div>{{hero.title}}</div>'
})
export class HeroComponent {
@Input() hero: Hero;
}
收集器把該表達(dá)式縮減成其等價(jià)的已折疊字符串:
'<div>{{hero.name}}</div><div>{{hero.title}}</div>'
下表中描述了收集器可以折疊以及不能折疊哪些表達(dá)式:
語法 |
可折疊? |
---|---|
對象字面量 |
是 |
數(shù)組字面量 |
是 |
展開數(shù)組字面量 |
否 |
函數(shù)調(diào)用 |
否 |
新建對象 |
否 |
屬性訪問 |
如果目標(biāo)對象也是可折疊的,則是 |
數(shù)組索引訪問 |
如果目標(biāo)數(shù)組和索引都是可折疊的,則是 |
引用標(biāo)識(shí)符 |
如果引用的是局部標(biāo)識(shí)符,則是 |
沒有替換表達(dá)式的模板字符串 |
是 |
有替換表達(dá)式的模板字符串 |
如果替換表達(dá)式是可折疊的,則是 |
字符串字面量 |
是 |
數(shù)字字面量 |
是 |
邏輯字面量 |
是 |
null 字面量 |
是 |
受支持的前綴運(yùn)算符 |
如果操作數(shù)是可折疊的,則是 |
受支持的二元運(yùn)算符 |
如果左操作數(shù)和右操作數(shù)都是可折疊的,則是 |
條件運(yùn)算符 |
如果條件是可折疊的,則是 |
括號(hào) |
如果表達(dá)式是可折疊的,則是 |
如果表達(dá)式是不可折疊的,那么收集器就會(huì)把它作為一個(gè) AST(抽象語法樹)寫入 ?.metadata.json
? 中,留給編譯器去解析。
收集器不會(huì)試圖理解它收集并輸出到 ?.metadata.json
? 中的元數(shù)據(jù),它所能做的只是盡可能準(zhǔn)確的表述這些元數(shù)據(jù),并在檢測到元數(shù)據(jù)中的語法違規(guī)時(shí)記錄這些錯(cuò)誤。解釋這些 ?.metadata.json
? 是編譯器在代碼生成階段要承擔(dān)的工作。
編譯器理解收集器支持的所有語法形式,但是它也可能拒絕那些雖然語法正確但語義違反了編譯器規(guī)則的元數(shù)據(jù)。
編譯器只能引用已導(dǎo)出的符號(hào)。
@Input()
?。只要語法有效,收集器就可以用 ?new
?來表示函數(shù)調(diào)用或?qū)ο髣?chuàng)建。但是,編譯器在后面可以拒絕生成對特定函數(shù)的調(diào)用或?qū)?b>特定對象的創(chuàng)建。
編譯器只能創(chuàng)建某些類的實(shí)例,僅支持核心裝飾器,并且僅支持對返回表達(dá)式的宏(函數(shù)或靜態(tài)方法)的調(diào)用。
編譯器動(dòng)作 |
詳情 |
---|---|
新建實(shí)例 |
編譯器只允許創(chuàng)建來自 |
支持的裝飾器 |
編譯器只支持來自 ? |
函數(shù)調(diào)用 |
工廠函數(shù)必須導(dǎo)出為命名函數(shù)。AOT 編譯器不支持用 Lambda 表達(dá)式(箭頭函數(shù))充當(dāng)工廠函數(shù)。 |
收集器接受任何只包含一個(gè) ?return
?語句的函數(shù)或靜態(tài)方法。編譯器也支持在返回表達(dá)式的函數(shù)或靜態(tài)函數(shù)中使用宏。
考慮下面的函數(shù):
export function wrapInArray<T>(value: T): T[] {
return [value];
}
你可以在元數(shù)據(jù)定義中調(diào)用 ?wrapInArray
?,因?yàn)樗祷氐谋磉_(dá)式的值滿足編譯器支持的 JavaScript 受限子集。
你還可以這樣使用 ?wrapInArray()
?:
@NgModule({
declarations: wrapInArray(TypicalComponent)
})
export class TypicalModule {}
編譯器會(huì)把這種用法處理成你以前的寫法:
@NgModule({
declarations: [TypicalComponent]
})
export class TypicalModule {}
Angular 的 ?RouterModule
?導(dǎo)出了兩個(gè)靜態(tài)宏函數(shù) ?forRoot
?和 ?forChild
?,以幫助聲明根路由和子路由。 查看這些方法的源碼,以了解宏函數(shù)是如何簡化復(fù)雜的 ?NgModule
?配置的。
編譯器會(huì)對含有 ?useClass
?、?useValue
?、?useFactory
?和 ?data
?的對象字面量進(jìn)行特殊處理,把用這些字段之一初始化的表達(dá)式轉(zhuǎn)換成一個(gè)導(dǎo)出的變量,并用它替換該表達(dá)式。這個(gè)重寫表達(dá)式的過程,會(huì)消除它們受到的所有限制,因?yàn)榫幾g器并不需要知道該表達(dá)式的值,它只要能生成對該值的引用就行了。
你可以這樣寫:
class TypicalServer {
}
@NgModule({
providers: [{provide: SERVER, useFactory: () => TypicalServer}]
})
export class TypicalModule {}
如果不重寫,這就是無效的,因?yàn)檫@里不支持 Lambda 表達(dá)式,而且 ?TypicalServer
?也沒有被導(dǎo)出。為了支持這種寫法,編譯器自動(dòng)把它重寫成了這樣:
class TypicalServer {
}
export const θ0 = () => new TypicalServer();
@NgModule({
providers: [{provide: SERVER, useFactory: θ0}]
})
export class TypicalModule {}
這就讓編譯器能在工廠中生成一個(gè)對 ?θ0
? 的引用,而不用知道 ?θ0
? 中包含的值到底是什么。
編譯器會(huì)在生成 ?.js
? 文件期間進(jìn)行這種重寫。它不會(huì)重寫 ?.d.ts
? 文件,所以 TypeScript 也不會(huì)把這個(gè)變量當(dāng)做一項(xiàng)導(dǎo)出,因此也就不會(huì)污染 ES 模塊中導(dǎo)出的 API。
Angular 編譯器最有用的功能之一就是能夠?qū)δ0逯械谋磉_(dá)式進(jìn)行類型檢查,在由于出錯(cuò)而導(dǎo)致運(yùn)行時(shí)崩潰之前就捕獲任何錯(cuò)誤。在模板類型檢查階段,Angular 模板編譯器會(huì)使用 TypeScript 編譯器來驗(yàn)證模板中的綁定表達(dá)式。
通過在該項(xiàng)目的 TypeScript 配置文件中的 ?"angularCompilerOptions"
? 中添加編譯器選項(xiàng) ?"fullTemplateTypeCheck"
?,可以顯式啟用本階段。
當(dāng)模板綁定表達(dá)式中檢測到類型錯(cuò)誤時(shí),進(jìn)行模板驗(yàn)證時(shí)就會(huì)生成錯(cuò)誤。這和 TypeScript 編譯器在處理 ?.ts
? 文件中的代碼時(shí)報(bào)告錯(cuò)誤很相似。
比如,考慮下列組件:
@Component({
selector: 'my-component',
template: '{{person.addresss.street}}'
})
class MyComponent {
person?: Person;
}
這會(huì)生成如下錯(cuò)誤:
my.component.ts.MyComponent.html(1,1): : Property 'addresss' does not exist on type 'Person'. Did you mean 'address'?
錯(cuò)誤信息中匯報(bào)的文件名 ?my.component.ts.MyComponent.html
? 是一個(gè)由模板編譯器生成出的合成文件,用于保存 ?MyComponent
?類的模板內(nèi)容。編譯器永遠(yuǎn)不會(huì)把這個(gè)文件寫入磁盤。這個(gè)例子中,這里的行號(hào)和列號(hào)都是相對于 ?MyComponent
?的 ?@Component
? 注解中的模板字符串的。如果組件使用 ?templateUrl
?來代替 ?template
?,這些錯(cuò)誤就會(huì)在 ?templateUrl
?引用的 HTML 文件中匯報(bào),而不是這個(gè)合成文件中。
錯(cuò)誤的位置是從包含出錯(cuò)的插值表達(dá)式的那個(gè)文本節(jié)點(diǎn)開始的。如果錯(cuò)誤是一個(gè)屬性綁定,比如 ?[value]="person.address.street"
?,錯(cuò)誤的位置就是那個(gè)包含錯(cuò)誤的屬性的位置。
驗(yàn)證使用 TypeScript 類型檢查器和提供給 TypeScript 編譯器的選項(xiàng)來控制類型驗(yàn)證的詳細(xì)程度。比如,如果指定了 ?strictTypeChecks
?,則會(huì)報(bào)告錯(cuò)誤以及下述錯(cuò)誤消息。
my.component.ts.MyComponent.html(1,1): : Object is possibly 'undefined'
在 ?ngIf
?指令中使用的表達(dá)式用來在 Angular 模板編譯器中窄化聯(lián)合類型,就像 TypeScript 中的 ?if
?表達(dá)式一樣。比如,要在上述模板中消除 ?Object is possibly 'undefined'
? 錯(cuò)誤,可以把它改成只在 ?person
?的值初始化過的時(shí)候才生成這個(gè)插值。
@Component({
selector: 'my-component',
template: ' {{person.address.street}} '
})
class MyComponent {
person?: Person;
}
使用 ?*ngIf
? 能讓 TypeScript 編譯器推斷出這個(gè)綁定表達(dá)式中使用的 ?person
?永遠(yuǎn)不會(huì)是 ?undefined
?。
使用 非空類型斷言操作符 可以在不方便使用 ?*ngIf
? 或 當(dāng)組件中的某些約束可以確保這個(gè)綁定表達(dá)式在求值時(shí)永遠(yuǎn)不會(huì)為空時(shí),防止出現(xiàn) ?Object is possibly 'undefined'
? 錯(cuò)誤。
在下面的例子中,?person
?和 ?address
?屬性總是一起出現(xiàn)的,如果 ?person
?非空,則 ?address
?也一定非空。沒有一種簡便的寫法可以向 TypeScript 和模板編譯器描述這種約束。但是這個(gè)例子中使用 ?address!.street
? 避免了報(bào)錯(cuò)。
@Component({
selector: 'my-component',
template: '<span *ngIf="person"> {{person.name}} lives on {{address!.street}} </span>'
})
class MyComponent {
person?: Person;
address?: Address;
setData(person: Person, address: Address) {
this.person = person;
this.address = address;
}
}
應(yīng)該保守點(diǎn)使用非空斷言操作符,因?yàn)閷韺M件的重構(gòu)可能會(huì)破壞這個(gè)約束。
這個(gè)例子中,更建議在 ?*ngIf
? 中包含對 ?address
?的檢查,代碼如下:
@Component({
selector: 'my-component',
template: '<span *ngIf="person && address"> {{person.name}} lives on {{address.street}} </span>'
})
class MyComponent {
person?: Person;
address?: Address;
setData(person: Person, address: Address) {
this.person = person;
this.address = address;
}
}
更多建議: