预先编译器, 英文全称是 Ahead-of-time compiler。由于 Angular 9 新版本的到来,CLI 应用程序默认情况下以 AOT 模式进行编译,其中包括模板类型检查,因此,我觉得有必要好好理解并总结一下 AOT 编译器的原理和流程。如需参考官方文档请前往 Angular - 预先(AOT)编译器。
大家都知道,Angular 的应用主要是由 components 和 HTML templates 组成。components 和 HTML templates 是 declarative 的代码, 而浏览器只接受 imperative 的代码 ( JavaScript ),因此它们无法被浏览器直接理解。这时 Angular 就需要自己的 compiler 来编译这些 declarative 的代码。那么,我们该什么时候编译这些代码呢?
Angular 给出了两种方案/模式:即时编译( JIT ) 和 预先( AOT )编译。顾名思义,即时编译,也就是 Just-in-time,是指编译器会在 runtime 编译应用。而预先编译则是指在构建( build )应用的时候进行编译。JIT 很好理解,但是为什么 Angular 会需要 AOT 这个编译模式呢?
知道了使用 AOT 的原因,我们现在可以来了解一下 AOT 的编译流程。
首先,在代码分析阶段,AOT 收集器( collector )起到来关键的作用。顾名思义,AOT 收集器负责收集整理 Angular 装饰器( decorators )的 元数据( metadata )。完成对代码的分析之后,AOT 编译器会为每一个 .ts
文件生成 .d.ts
文件,.d.ts
文件又叫类型定义文件。该文件包含原 .ts
文件中属性方法的类型信息,它可以帮助编译器生成 imperative 代码。
AOT 收集器在该阶段会把收集到的 metadata 信息输出到 .metadata.json
的文件中。每个 .d.ts
文件对应一个 .metadata.json
文件。那么,Angular metadata 有什么作用呢?
形象地讲,.metadata.json
文件可以被看成包含了一个装饰器全部 metadata 的全景图,就像是 Abstract syntax tree ( AST )一样。.metadata.json
文件包含了原 .ts
文件中被 template 编译器需要的,但没有在 .d.ts
文件里的信息。举个例子,这些信息可能包括 component 里的 template。
值得一提的是,AOT 编译器其实可以被看作 JavaScript 的一个子集,它不能完整地理解 JavaScript 语法。举个例子,AOT 编译器不支持 Lambda 函数,也就是箭头函数。假设我们想为一个 service 创建一个自己的 provider:
@Component({
...
providers: [{provide: server, useFactory: () => new Server()}]
})
在这个例子里, provider 关键词接受了一个独一无二的 Injection Token,而 useFactory 接受了一个返回 service 实例的 Lambda 函数。这段代码没有任何错误,但是 AOT 编译器却无法理解 Lambda 表达式。在 Angular 5 之前,编译器会抛出一个错误。在 Angular 5 及之后的版本,它会自动把上面那段程序转换为以下代码:
export function serverFactory() {
return new Server();
}
@Component({
...
providers: [{provide: server, useFactory: serverFactory}]
})
更值得一提的是,你可以注意到上面例子中 serverFactory()
方法使用了 export
关键字,这是因为 AOT 编译器不支持没有被 exported 的 function。
以下是一部分 AOT 编译器支持的语法:
{ key1 : value1, key2 : value2 }
[ item1, item2, item3 ]
expression ? value1 : value2
还有一个有趣的现象,叫做代码折叠( Code folding )。与其把完整的原始表达式记录到 .metadata.json
文件,AOT 收集器会在收集期间执行一些表达式。比如 .ts
文件中有表达式 1 + 5 + 10
,收集器则会执行这个表达式,得到结果 16
,并记录该结果,而非表达式 1 + 5 + 10
。任何能被收集器执行简化的表达式都是可折叠的( foldable )。类似的,收集器还可以把模块局部 const
变量、var
变量和 let
变量以内联的方式把他们折叠进 metadata 中,从而把这些 const
变量、var
变量和 let
变量的声明从 .metadata.json
文件中移除。如果表示式无法折叠,收集器就会把它原原本本地以 AST 的形式记录进 .metadata.json
文件,在接下来的阶段让编译器去解析。
想要查阅所有的表达式语法限制以及可折叠的语法,请前往Angular - 预先(AOT)编译器。
AOT 收集器到此为止算是完成了它的任务,它收集了所有 .ts
文件里面的 metadata 并把他们记录进了 .metadata.json
文件。 这个过程中收集器不会试图去解析这些 metadata,而是只负责尽量准确地表述它们,同时记录任何检测到的 metadata 里的语法错误( syntax violation )。
接下来,就到了代码生成的阶段。在这个阶段,AOT 编译器会负责去解析这些 .metadata.json
文件。在解析过程中会抛出所有检测到的语义错误( semantic errors )。这些错误中值得提到的是 public errors。public errors 是指在 HTML template 里使用的变量在 .ts
文件里并没有设置 public
关键字。考虑以下场景:
app.component.html
:
<span>{{data}}</span>
app.component.ts
:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
private data = 'Some data';
}
从上面的例子可以看出,程序的本来目的是想让 app.component.html
的 data 变量绑定到 app.component.ts
里的 data。但是由于 data 被设有 private
关键字,数据绑定会失败,而编译器则会对这段程序抛出错误。由此我们可以总结一下数据绑定:
对于 decorated component 的类成员:
public
。@Input
property 也必须是 public
。在代码分析阶段,只要没有语法错误,AOT 收集器就可以用 new 来表示 function call 或是 对象创建。但是这并不能保证 AOT 编译器在代码生成阶段会生成对应的 function call 或是 对象创建的代码。具体的说,AOT 编译器仅支持 core 装饰器,并且仅支持调用会返回表达式的macros (函数或是静态方法)。
@angular/core
的 InjectionToken
类创建实例。@angular/core
的 Angular 装饰器的 metadata。在代码分析阶段,收集器会接受任何只包含一个 return
语句的 macros。但是就像之前提到的,编译器仅支持会返回表达式的 macros。
首先,在 tsconfig.json
文件中的 angularCompilerOptions
中添加编译器选项 fullTemplateTypeCheck
可以开启该阶段。在 Angular 9 里模板类型检查阶段默认启用。
该阶段启用后,紧接着代码生成,AOT 编译器会检测 template types。它会使用 TypeScript 编译器来验证 templates 里面被绑定的表达式(变量或 function )。这样保证里在 runtime 程序运行崩溃之前就先捕获错误。
考虑以下场景,AOT 编译器会检查在 HTML template 里面使用的 isEven()
function 是否在 TS 文件中定义:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template:'<span *ngIf="isEven(2)">Even number</span>',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
title = 'App component';
}
由于 isEven()
function 没有定义,编译器会抛出错误 isEven is not a function
。这也和 TypeScript 编译器在处里 .ts
文件中的代码时报告错误的方式很类似。
在推出 Angular Ivy之后,模板类型检查器已经被全面更新,更强大更严格的它可以捕获更多新类型的错误。但是同时这样也意味着能够在 Ivy 之前的 View Engine 通过编译的 templates 可能无法通过更严格的 Ivy 的模版类型检查。因为这个原因,Angular 9 中并没有默认启用这更严格的模版类型检查。
目前来说,模板类型检查有三个等级的模式:Basic Mode,Full Mode 和 Strict Mode。当前为哪个等级取决于 tsconfig.json
文件中编译器选项 fullTemplateTypeCheck
和 strictTemplates
的值。
如果 fullTemplateTypeCheck
设置为 false
,那么当前处于 Basic Mode。在该模式下,AOT 编译器仅检测 templates 中最顶层的表达式。如果在验证 <app-people [name]="user.name">
的时候,编译器只会验证 user
是否是 component class 的 property 和 user
是否是具有 name
的对象。它不会验证 user.name
的值是否可以复制给 app-people
组建的 name
property。除此之外,Basic Mode 下编译器不会检查嵌入式视图,例如 *ngIf
,*ngFor
和其他 <ng-template>
嵌入式视图、无法弄清 #refs
的类型、pipes
的结果、事件绑定中 $event
的类型等等。
如果 fullTemplateTypeCheck
设置为 true
,那么当前处于 Full Mode。Full Mode 下 AOT 编译器则会更加主动地检测之前提到的嵌入式视图、pipes
的结果、directives 和 pipes
的 local references 是否有正确类型。
如果 strictTemplates
设为 true
,那么当前处于 Strict Mode。Strict Mode 就是 Angular 9 推出的更强大的 Full Mode 的超集。注意 strictTemplates
会取代掉 fullTemplateTypeCheck
,并且 Strict Mode 仅在 Ivy 下有效。在 Strict Mode 下,编译器还会:
@Input
。*ngFor
进行正确的类型检查)。$event
的正确类型。document.createElement
),推断出对 DOM 元素的局部引用的正确类型。
import { Component, EventEmitter, Output } from '@angular/core';
@Component({
selector: 'text-editor',
template: `
<textarea (keyup)="emitWordCount($event)"></textarea>
`
})
export class TextEditorComponent {
@Output() countUpdate = new EventEmitter<number>();
emitWordCount(e: Event) {
this.countUpdate.emit(
(e.target.value.match(/\S+/g) || []).length);
}
}
出现错误:
Angular 11+
Open tsconfig.json and disable strictTemplates.
"angularCompilerOptions": { .... ........ "strictTemplates": false }
<div class="form-inline float-left mr-1">
<select class="form-control" [value]="productsPerPage"
(change)="changePageSize($event.target.value)">
<option value="3">3 per Page</option>
<option value="4">4 per Page</option>
<option value="6">6 per Page</option>
<option value="8">8 per Page</option>
</select>
</div>
changePageSize($event.target.value) 也报同样的错误。设置strictTemplates为false,ng build通过.