Angular 預(yù)先編譯

2022-07-14 11:39 更新

預(yù)先(AOT)編譯器

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)用。

觀看 Alex Rickabaugh 在 AngularConnect 2019 解釋 Angular 編譯器的演講。

下面是你可能要使用 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?。

AOT 工作原理

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í)例上。

編譯的各個(gè)階段

AOT 編譯分為三個(gè)階段。

階段

詳情

1

代碼分析

在此階段,TypeScript 編譯器和AOT 收集器會(huì)創(chuàng)建源代碼的表示。收集器不會(huì)嘗試解釋它收集的元數(shù)據(jù)。它會(huì)盡可能地表示元數(shù)據(jù),并在檢測到元數(shù)據(jù)語法違規(guī)時(shí)記錄錯(cuò)誤。

2

代碼生成

在此階段,編譯器的 StaticReflector 會(huì)解釋在階段 1 收集的元數(shù)據(jù),對元數(shù)據(jù)執(zhí)行額外的驗(yàn)證,如果檢測到違反元數(shù)據(jù)限制,則會(huì)拋出錯(cuò)誤。

3

模板類型檢查

在此可選階段,Angular 模板編譯器使用 TypeScript 編譯器來驗(yàn)證模板中的綁定表達(dá)式。你可以通過設(shè)置 fullTemplateTypeCheck 配置選項(xiàng)來明確啟用此階段。

元數(shù)據(jù)的限制

你只能使用 TypeScript 的一個(gè)子集書寫元數(shù)據(jù),它必須滿足下列限制:

  • 表達(dá)式語法只支持 JavaScript 的一個(gè)有限的子集
  • 只能引用代碼收縮后導(dǎo)出的符號(hào)
  • 只能調(diào)用編譯器支持的函數(shù)
  • 被裝飾和用于數(shù)據(jù)綁定的類成員必須是公共(public)的

關(guān)于準(zhǔn)備 AOT 編譯應(yīng)用程序的其它準(zhǔn)則和說明,參閱 Angular:編寫 AOT 友好的應(yīng)用程序。

AOT 編譯中的錯(cuò)誤通常是由于元數(shù)據(jù)不符合編譯器的要求而發(fā)生的(下面將更全面地介紹)。

配置 AOT 編譯

你可以在 ?tsconfig.jsonTypeScript 配置文件中提供控制編譯過程的選項(xiàng)。

階段 1:分析

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 接口的集合。

表達(dá)式語法限制

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í)踐。

不要有箭頭函數(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? 中,留給編譯器去解析。

階段 2:代碼生成

收集器不會(huì)試圖理解它收集并輸出到 ?.metadata.json? 中的元數(shù)據(jù),它所能做的只是盡可能準(zhǔn)確的表述這些元數(shù)據(jù),并在檢測到元數(shù)據(jù)中的語法違規(guī)時(shí)記錄這些錯(cuò)誤。解釋這些 ?.metadata.json? 是編譯器在代碼生成階段要承擔(dān)的工作。

編譯器理解收集器支持的所有語法形式,但是它也可能拒絕那些雖然語法正確語義違反了編譯器規(guī)則的元數(shù)據(jù)。

公共符號(hào)

編譯器只能引用已導(dǎo)出的符號(hào)。

  • 帶有裝飾器的類成員必須是公開的。你不可能把一個(gè)私有或內(nèi)部使用的屬性做成 ?@Input()?。
  • 數(shù)據(jù)綁定的屬性同樣必須是公開的

支持的類和函數(shù)

只要語法有效,收集器就可以用 ?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)建來自 @angular/core 的 InjectionToken 類創(chuàng)建實(shí)例。

支持的裝飾器

編譯器只支持來自 ?@angular/core? 模塊的 Angular 裝飾器的元數(shù)據(jù)。

函數(shù)調(diào)用

工廠函數(shù)必須導(dǎo)出為命名函數(shù)。AOT 編譯器不支持用 Lambda 表達(dá)式(箭頭函數(shù))充當(dāng)工廠函數(shù)。

函數(shù)和靜態(tài)方法調(diào)用

收集器接受任何只包含一個(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 ?配置的。

元數(shù)據(jù)重寫

編譯器會(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。

階段 3:模板類型檢查

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;
  }
}


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)