Angular 的 预先(AOT)编译器
预先编译器, 英文全称是 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 模式的定义所讲的一样,由于 declarative 的代码会被预先编译,浏览器可以直接使用这些可以直接执行的 imperative 代码,立即给用户呈现应用。
- 更早检查出 template 错误。 由于需要预编译,AOT compiler 会在构建阶段就检测到 template 的绑定错误,并把这些错误提前报告给我们写程序的人,而不是等到 runtime 编译才让用户发现这些错误。
- 更高的 client-side 安全性。 由于 templates 和 components 在给 client side 接触到之前就被预先编译成了 JavaScript,client side 没有办法读取到 templates,HTML 和 JavaScript 的解析也不会存在很大的危险性,这样也让 Client-side injection attacks 也会变得更加困难。
- 需要下载的 Angular Framework size 变得更小。 由于应用程序被预编译,Angular 编译器也就无需被下载。应用程序负荷( payload )大大减少。
- 更少的异步请求。 AOT 编译器会内联 HTML template 和 CSS style sheets,其中的单独的 ajax 请求也会随之被消除。
知道了使用 AOT 的原因,我们现在可以来了解一下 AOT 的编译流程。
代码分析( Code Analysis )
首先,在代码分析阶段,AOT 收集器( collector )起到来关键的作用。顾名思义,AOT 收集器负责收集整理 Angular 装饰器( decorators )的 元数据( metadata )。完成对代码的分析之后,AOT 编译器会为每一个
.ts
文件生成
.d.ts
文件,
.d.ts
文件又叫类型定义文件。该文件包含原
.ts
文件中属性方法的类型信息,它可以帮助编译器生成 imperative 代码。
AOT 收集器在该阶段会把收集到的 metadata 信息输出到
.metadata.json
的文件中。每个
.d.ts
文件对应一个
.metadata.json
文件。那么,Angular metadata 有什么作用呢?
- 告诉 Angular 如何创建应用 classes 的实例。
- 告诉 Angular 如何在 runtime 跟这些实例进行 interaction。
形象地讲,
.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 编译器支持的语法:
-
对象字面量 ( Literal object ):
{ key1 : value1, key2 : value2 }
-
数组字面量 ( Literal Array ):
[ item1, item2, item3 ]
- Null 字面量 ( Literal Null )
-
条件字面量 ( Conditional operator ):
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)编译器 。
代码生成( Code Generation )
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。 - Function calls :Factory functions 必须被 exported,必须是被命名 functions。不支持 Lambda 函数充当 Factory functions。
在代码分析阶段,收集器会接受任何只包含一个
return
语句的 macros。但是就像之前提到的,编译器仅支持会返回表达式的 macros。
模板类型检查( Template type checking )
首先,在
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',