Angular 預先編譯

2022-07-14 11:39 更新

預先(AOT)編譯器

Angular 應用主要由組件及其 HTML 模板組成。由于瀏覽器無法直接理解 Angular 所提供的組件和模板,因此 Angular 應用程序需要先進行編譯才能在瀏覽器中運行。

在瀏覽器下載和運行代碼之前的編譯階段,Angular 預先(AOT)編譯器會先把你的 Angular HTML 和 TypeScript 代碼轉換成高效的 JavaScript 代碼。在構建期間編譯應用可以讓瀏覽器中的渲染更快速。

本指南中解釋了如何指定元數(shù)據(jù),并使用一些編譯器選項以借助 AOT 編譯器來更有效的編譯應用。

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

下面是你可能要使用 AOT 的部分原因。

原因

詳情

更快的渲染方式

使用 AOT,瀏覽器會下載應用程序的預編譯版本。瀏覽器加載可執(zhí)行代碼,以便立即渲染應用程序,而無需等待先編譯應用程序。

更少的異步請求

編譯器在應用程序 JavaScript 中內聯(lián)外部 HTML 模板和 CSS 樣式表,消除對這些源文件的單個 ajax 請求。

更小的 Angular 框架下載大小

如果應用程序已被編譯,則無需下載 Angular 編譯器。編譯器大約是 Angular 本身的一半,因此省略它會大大減少應用程序的體積。

及早檢測模板錯誤

AOT 編譯器會在用戶看到之前在構建步驟中檢測并報告模板綁定錯誤。

更好的安全性

AOT 會在 HTML 模板和組件提供給客戶端之前就將它們編譯為 JavaScript 文件。由于沒有要讀取的模板,也沒有危險的客戶端 HTML 或 JavaScript 求值,因此注入攻擊的機會更少。

選擇編譯器

Angular 提供了兩種方式來編譯你的應用:

ANGULAR 編譯方式

詳情

即時 (JIT)

當運行時在瀏覽器中編譯你的應用程序。在 Angular 8 之前,這是默認值。

預先 (AOT)

在構建時編譯你的應用程序和庫。這是從 Angular 9 開始的默認值。

當運行 CLI 命令 ?ng build? (只構建) 或 ?ng serve? (構建并啟動本地服務器) 時,編譯類型(JIT 或 AOT)取決于你在 ?angular.json? 中的構建配置所指定的 ?aot ?屬性。默認情況下,對于新的 CLI 應用,其 ?aot ?為 ?true?。

AOT 工作原理

Angular AOT 編譯器會提取元數(shù)據(jù)來解釋應由 Angular 管理的應用程序部分。你可以在裝飾器(比如 ?@Component()? 和 ?@Input()?)中顯式指定元數(shù)據(jù),也可以在被裝飾的類的構造函數(shù)聲明中隱式指定元數(shù)據(jù)。元數(shù)據(jù)告訴 Angular 要如何構造應用程序類的實例并在運行時與它們進行交互。

在下列范例中,?@Component()? 元數(shù)據(jù)對象和類的構造函數(shù)會告訴 Angular 如何創(chuàng)建和顯示 ?TypicalComponent ?的實例。

@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 ?生成一個工廠。當它需要創(chuàng)建 ?TypicalComponent ?的實例時,Angular 調用這個工廠,工廠會生成一個新的可視元素,并且把它(及其依賴)綁定到組件類的一個新實例上。

編譯的各個階段

AOT 編譯分為三個階段。

階段

詳情

1

代碼分析

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

2

代碼生成

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

3

模板類型檢查

在此可選階段,Angular 模板編譯器使用 TypeScript 編譯器來驗證模板中的綁定表達式。你可以通過設置 fullTemplateTypeCheck 配置選項來明確啟用此階段。

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

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

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

關于準備 AOT 編譯應用程序的其它準則和說明,參閱 Angular:編寫 AOT 友好的應用程序。

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

配置 AOT 編譯

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

階段 1:分析

TypeScript 編譯器會做一些初步的分析工作,它會生成類型定義文件?.d.ts?,其中帶有類型信息,Angular 編譯器需要借助它們來生成代碼。 同時,AOT 收集器(collector) 會記錄 Angular 裝飾器中的元數(shù)據(jù),并把它們輸出到?.metadata.json?文件中,和每個 ?.d.ts? 文件相對應。

你可以把 ?.metadata.json? 文件看做一個包括全部裝飾器的元數(shù)據(jù)的全景圖,就像抽象語法樹 (AST)一樣。

Angular 的 ?schema.ts? 會將 JSON 格式描述為 TypeScript 接口的集合。

表達式語法限制

AOT 收集器只能理解 JavaScript 的一個子集。定義元數(shù)據(jù)對象時要遵循下列語法限制:

語法

范例

對象字面量

{cherry: true, apple: true, mincemeat: false}

數(shù)組字面量

['cherries', 'flour', 'sugar']

展開數(shù)組字面量

['apples', 'flour', ...]

函數(shù)調用

bake(ingredients)

新建對象

new Oven()

屬性訪問

pie.slice

數(shù)組索引訪問

ingredients[0]

引用標識符

Component

模板字符串

`pie is ${multiplier} times better than cake`

字符串字面量

'pi'

數(shù)字字面量

3.14153265

邏輯字面量

true

null 字面量

null

受支持的前綴運算符

!cake

受支持的二元運算符

a+b

條件運算符

a ? b : c

括號

(a+b)

如果表達式使用了不支持的語法,收集器就會往 ?.metadata.json? 文件中寫入一個錯誤節(jié)點。稍后,如果編譯器用到元數(shù)據(jù)中的這部分內容來生成應用代碼,它就會報告這個錯誤。

如果你希望 ?ngc ?立即匯報這些語法錯誤,而不要生成帶有錯誤信息的 ?.metadata.json? 文件,可以到 TypeScript 的配置文件中設置 ?strictMetadataEmit ?選項。

"angularCompilerOptions": {
  …
  "strictMetadataEmit" : true
}

Angular 庫通過這個選項來確保所有的 Angular ?.metadata.json? 文件都是干凈的。當你要構建自己的代碼庫時,這也同樣是一項最佳實踐。

不要有箭頭函數(shù)

AOT 編譯器不支持函數(shù)表達式箭頭函數(shù),也叫 lambda 函數(shù)。

考慮如下組件裝飾器:

@Component({
  …
  providers: [{provide: server, useFactory: () => new Server()}]
})

AOT 的收集器不支持在元數(shù)據(jù)表達式中出現(xiàn)箭頭函數(shù) ?() => new Server()?。它會在該函數(shù)中就地生成一個錯誤節(jié)點。稍后,當編譯器解釋該節(jié)點時,它就會報告一個錯誤,讓你把這個箭頭函數(shù)轉換成一個導出的函數(shù)。

你可以把它改寫成這樣來修復這個錯誤:

export function serverFactory() {
  return new Server();
}

@Component({
  …
  providers: [{provide: server, useFactory: serverFactory}]
})

在版本 5 和更高版本中,編譯器會在發(fā)出 ?.js? 文件時自動執(zhí)行此重寫。

代碼折疊

編譯器只會解析到已導出符號的引用。收集器可以在收集期間執(zhí)行表達式,并用其結果記錄到 ?.metadata.json? 中(而不是原始表達式中)。這樣可以讓你把非導出符號的使用限制在表達式中。

比如,收集器可以估算表達式 ?1 + 2 + 3 + 4? 并將其替換為結果 ?10?。這個過程稱為?折疊???梢杂眠@種方式簡化的表達式是可折疊的

收集器可以計算對模塊局部變量的 ?const ?聲明和初始化過的 ?var ?和 ?let ?聲明,并從 ?.metadata.json? 文件中移除它們。

考慮下列組件定義:

const template = '<div>{{hero.name}}</div>';

@Component({
  selector: 'app-hero',
  template: template
})
export class HeroComponent {
  @Input() hero: Hero;
}

編譯器不能引用 ?template ?常量,因為它是未導出的。但是收集器可以通過內聯(lián) ?template ?常量的方式把它折疊進元數(shù)據(jù)定義中。最終的結果和你以前的寫法是一樣的:

@Component({
  selector: 'app-hero',
  template: '<div>{{hero.name}}</div>'
})
export class HeroComponent {
  @Input() hero: Hero;
}

這里沒有對 ?template ?的引用,因此,當編譯器稍后對位于 ?.metadata.json? 中的收集器輸出進行解釋時,不會再出問題。

你還可以通過把 ?template ?常量包含在其它表達式中來讓這個例子深入一點:

const template = '<div>{{hero.name}}</div>';

@Component({
  selector: 'app-hero',
  template: template + '<div>{{hero.title}}</div>'
})
export class HeroComponent {
  @Input() hero: Hero;
}

收集器把該表達式縮減成其等價的已折疊字符串:

'<div>{{hero.name}}</div><div>{{hero.title}}</div>'

可折疊的語法

下表中描述了收集器可以折疊以及不能折疊哪些表達式:

語法

可折疊?

對象字面量

數(shù)組字面量

展開數(shù)組字面量

函數(shù)調用

新建對象

屬性訪問

如果目標對象也是可折疊的,則是

數(shù)組索引訪問

如果目標數(shù)組和索引都是可折疊的,則是

引用標識符

如果引用的是局部標識符,則是

沒有替換表達式的模板字符串

有替換表達式的模板字符串

如果替換表達式是可折疊的,則是

字符串字面量

數(shù)字字面量

邏輯字面量

null 字面量

受支持的前綴運算符

如果操作數(shù)是可折疊的,則是

受支持的二元運算符

如果左操作數(shù)和右操作數(shù)都是可折疊的,則是

條件運算符

如果條件是可折疊的,則是

括號

如果表達式是可折疊的,則是

如果表達式是不可折疊的,那么收集器就會把它作為一個 AST(抽象語法樹)寫入 ?.metadata.json? 中,留給編譯器去解析。

階段 2:代碼生成

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

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

公共符號

編譯器只能引用已導出的符號。

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

支持的類和函數(shù)

只要語法有效,收集器就可以用 ?new ?來表示函數(shù)調用或對象創(chuàng)建。但是,編譯器在后面可以拒絕生成對特定函數(shù)的調用或對特定對象的創(chuàng)建。

編譯器只能創(chuàng)建某些類的實例,僅支持核心裝飾器,并且僅支持對返回表達式的宏(函數(shù)或靜態(tài)方法)的調用。

編譯器動作

詳情

新建實例

編譯器只允許創(chuàng)建來自 @angular/core 的 InjectionToken 類創(chuàng)建實例。

支持的裝飾器

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

函數(shù)調用

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

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

收集器接受任何只包含一個 ?return ?語句的函數(shù)或靜態(tài)方法。編譯器也支持在返回表達式的函數(shù)或靜態(tài)函數(shù)中使用。

考慮下面的函數(shù):

export function wrapInArray<T>(value: T): T[] {
  return [value];
}

你可以在元數(shù)據(jù)定義中調用 ?wrapInArray?,因為它所返回的表達式的值滿足編譯器支持的 JavaScript 受限子集。

你還可以這樣使用 ?wrapInArray()?:

@NgModule({
  declarations: wrapInArray(TypicalComponent)
})
export class TypicalModule {}

編譯器會把這種用法處理成你以前的寫法:

@NgModule({
  declarations: [TypicalComponent]
})
export class TypicalModule {}

Angular 的 ?RouterModule ?導出了兩個靜態(tài)宏函數(shù) ?forRoot ?和 ?forChild?,以幫助聲明根路由和子路由。 查看這些方法的源碼,以了解宏函數(shù)是如何簡化復雜的 ?NgModule ?配置的。

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

編譯器會對含有 ?useClass?、?useValue?、?useFactory ?和 ?data ?的對象字面量進行特殊處理,把用這些字段之一初始化的表達式轉換成一個導出的變量,并用它替換該表達式。這個重寫表達式的過程,會消除它們受到的所有限制,因為編譯器并不需要知道該表達式的值,它只要能生成對該值的引用就行了。

你可以這樣寫:

class TypicalServer {

}

@NgModule({
  providers: [{provide: SERVER, useFactory: () => TypicalServer}]
})
export class TypicalModule {}

如果不重寫,這就是無效的,因為這里不支持 Lambda 表達式,而且 ?TypicalServer ?也沒有被導出。為了支持這種寫法,編譯器自動把它重寫成了這樣:

class TypicalServer {

}

export const θ0 = () => new TypicalServer();

@NgModule({
  providers: [{provide: SERVER, useFactory: θ0}]
})
export class TypicalModule {}

這就讓編譯器能在工廠中生成一個對 ?θ0? 的引用,而不用知道 ?θ0? 中包含的值到底是什么。

編譯器會在生成 ?.js? 文件期間進行這種重寫。它不會重寫 ?.d.ts? 文件,所以 TypeScript 也不會把這個變量當做一項導出,因此也就不會污染 ES 模塊中導出的 API。

階段 3:模板類型檢查

Angular 編譯器最有用的功能之一就是能夠對模板中的表達式進行類型檢查,在由于出錯而導致運行時崩潰之前就捕獲任何錯誤。在模板類型檢查階段,Angular 模板編譯器會使用 TypeScript 編譯器來驗證模板中的綁定表達式。

通過在該項目的 TypeScript 配置文件中的 ?"angularCompilerOptions"? 中添加編譯器選項 ?"fullTemplateTypeCheck"?,可以顯式啟用本階段。

當模板綁定表達式中檢測到類型錯誤時,進行模板驗證時就會生成錯誤。這和 TypeScript 編譯器在處理 ?.ts? 文件中的代碼時報告錯誤很相似。

比如,考慮下列組件:

@Component({
  selector: 'my-component',
  template: '{{person.addresss.street}}'
})
class MyComponent {
  person?: Person;
}

這會生成如下錯誤:

my.component.ts.MyComponent.html(1,1): : Property 'addresss' does not exist on type 'Person'. Did you mean 'address'?

錯誤信息中匯報的文件名 ?my.component.ts.MyComponent.html? 是一個由模板編譯器生成出的合成文件,用于保存 ?MyComponent ?類的模板內容。編譯器永遠不會把這個文件寫入磁盤。這個例子中,這里的行號和列號都是相對于 ?MyComponent ?的 ?@Component? 注解中的模板字符串的。如果組件使用 ?templateUrl ?來代替 ?template?,這些錯誤就會在 ?templateUrl ?引用的 HTML 文件中匯報,而不是這個合成文件中。

錯誤的位置是從包含出錯的插值表達式的那個文本節(jié)點開始的。如果錯誤是一個屬性綁定,比如 ?[value]="person.address.street"?,錯誤的位置就是那個包含錯誤的屬性的位置。

驗證使用 TypeScript 類型檢查器和提供給 TypeScript 編譯器的選項來控制類型驗證的詳細程度。比如,如果指定了 ?strictTypeChecks?,則會報告錯誤以及下述錯誤消息。

my.component.ts.MyComponent.html(1,1): : Object is possibly 'undefined'

類型窄化

在 ?ngIf ?指令中使用的表達式用來在 Angular 模板編譯器中窄化聯(lián)合類型,就像 TypeScript 中的 ?if ?表達式一樣。比如,要在上述模板中消除 ?Object is possibly 'undefined'? 錯誤,可以把它改成只在 ?person ?的值初始化過的時候才生成這個插值。

@Component({
  selector: 'my-component',
  template: ' {{person.address.street}} '
})
class MyComponent {
  person?: Person;
}

使用 ?*ngIf? 能讓 TypeScript 編譯器推斷出這個綁定表達式中使用的 ?person ?永遠不會是 ?undefined?。

非空類型斷言操作符

使用 非空類型斷言操作符 可以在不方便使用 ?*ngIf? 或 當組件中的某些約束可以確保這個綁定表達式在求值時永遠不會為空時,防止出現(xiàn) ?Object is possibly 'undefined'? 錯誤。

在下面的例子中,?person ?和 ?address ?屬性總是一起出現(xiàn)的,如果 ?person ?非空,則 ?address ?也一定非空。沒有一種簡便的寫法可以向 TypeScript 和模板編譯器描述這種約束。但是這個例子中使用 ?address!.street? 避免了報錯。

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

應該保守點使用非空斷言操作符,因為將來對組件的重構可能會破壞這個約束。

這個例子中,更建議在 ?*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;
  }
}


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號