Angular 9 Ivy 编译器简介
目录
免责声明:
本文包含对 Angular 工作原理的初步探究,包括阅读部分源代码、调试一个简单应用程序以及了解编译器工作原理。部分术语或定义可能存在错误。
Angular 为什么要有编译器呢?
编译器的主要工作是将你编写的模板转换成 Angular 运行时所需的代码。Alex Rickabaugh - Angular Connect 2019
在 Angular 中,开发人员以声明方式编写模板,例如渲染内容、绑定等,但并未明确说明运行时的具体实现方式,也没有关于变更检测机制如何工作的描述。Angular 运行时无法理解声明式模板语法,因此必须将其转换为运行时可以在浏览器中运行的内容。
编译器采用声明性 Angular 语法并将其转换为命令式代码。
为什么还需要编译器?手动操作不行吗?
- 大量的样板代码,最好关注业务逻辑而不是底层工作原理。
- Angular 团队可以通过发布 wrt 浏览器演进命令式代码来优化和改进。
关于 Angular Ivy 的更详细介绍可以参见
行话
- Angular 编译器: Angular 渲染架构的一部分,它将模板和装饰器转换为 Angular 运行时可以理解的内容。
- Angular Runtime: Angular 渲染架构的一部分,用于运行 Angular 应用程序。
- Angular 模板声明语法:描述视图应该是什么样子、视图应该显示什么,但不描述如何显示。
- Angular 应用程序命令式代码:描述如何通过一系列 JavaScript 指令/命令呈现视图。
- 元数据:封装在装饰器中的一组数据,用于描述装饰器本身所代表的实体。例如,组件包含选择器、模板等。元数据随后会被编译器重用,并放入定义文件
.d.ts
(即组件 API)中。基本上,元数据有助于保留从文件中删除的信息.js
。
快速概览
根据实现情况,新 Angular 编译器上所做的工作可分为三类:
@angular/compiler-cli
:TypeScript 转换器管道,包含两个 CLI 工具ngtsc
:Angular TypeScript 编译器会查找类似的Angular 装饰器@Component
,并用特定的 Angular Runtime 指令/对应部分(例如)替换它们ɵɵdefineComponent
。ngcc
:Angular 兼容性编译器将 pre-ivy 模块转换为 ivy-module,甚至可以作为 Webpack 等代码加载器的一部分运行,以便node_modules
动态地转换软件包。
@angular/compiler
:Ivy 编译器,将装饰器转换为 Ivy。@angular/core
:可以通过 进行转换的装饰器@angular/compiler
。
Angular Ivy 编译器模型
Ivy 模型预见到将诸如@Injectable
、等Angular 装饰器编译@Component
为静态属性。
所有装饰器都不需要应用程序的全局知识,除了@Component
需要来自 的信息@NgModule
。在模块中,组件模板使用的其他选择器已声明,因此该模块导出的传递闭包导入了。
如果没有这些信息,组件 def ( ɵcmp
) 就无法正确生成。
考虑以下Welcome to Angular!
应用:
@Component({
selector: 'app-root',
template: `
<div style="text-align:center">
<h1>
Welcome to {{ title }}!
</h1>
</div>
`,
styleUrls: []
})
export class AppComponent {
@Input() title = 'Angular';
}
前面的代码以诸如和 之类的装饰器为特征,它们是Angular 模板声明语法的一部分,为开发人员提供了一种简单的模板语法,可以@Component
@Input
- 编写控制流语句,例如
for loop
和if
条件语句, - 并在控制器中声明的变量和模板中使用的变量之间进行数据绑定。
提示:
对于大多数开发人员来说,绑定只是在模板和控制器之间使用相同的变量名,但有一种机制可以在运行时管理变更检测。这是一件很棒的事情,Angular 编译器会自动添加此机制。样板代码更少,模板更易于阅读,也更不容易出错。
Angular运行时是一系列 JavaScript 指令/函数的集合,它们能够将组件模板渲染到 DOM 中,并在模型发生变更时响应变更检测(MVC)。所有内容都必须用 JavaScript 编写,并且随时可以运行,因此 Angular 的声明式语法会被翻译成这些指令。
为了更清晰地理解,请尝试开发一个标准的 Web 组件:当开发人员需要处理模板时,他必须使用 DOM API 来创建元素并将其附加到 DOM,编写一些代码来检测模型的变化并更新视图。在 Angular 代码中,这些操作并不存在,因为模板被转换成JavaScript 命令式代码(一系列 JavaScript 指令,是 Angular 运行时的一部分),这些指令在被调用时会在浏览器中创建组件。
大部分繁琐而重复的工作都是由 Angular 编译器与 Angular 运行时一起完成的。
编译器实现解耦
开发人员只需通过 Angular 声明语法编写 Angular 模块和组件,即“什么”,但不知道/不关心“如何”,即运行时如何执行:编译器可以将“什么”与“如何”分离。
这种方法有多个优点:
- 尽量减少或消除副作用,使开发人员的生活更简单;
- 如果 Angular 渲染架构不断发展,则只需进行少量更改或无需更改即可使用最新版本;
- 随着 EcmaScript 的不断变化,浏览器也越来越频繁地变化,因此模板的呈现方式也会随之改变,例如优化 Web 性能;
- 模板和装饰器可以根据运行代码的平台进行不同的编译,例如支持模块的 ES5 或 ES6。
简单的项目设置
Angular 9 已于近期发布,生成一个运行 Angular 9 的简单项目:
npm install -g @angular/cli // to install the Angular CLI
ng new angular-nine-ivy // or the name you want
如果您已经有一个包含一些更改的工作区,则必须提交或存储它们,否则添加标志--allow-dirty
。
如果您已经拥有旧的 Angular CLI,那么更好的方法似乎是卸载旧版本并全局安装新的版本。
让我们编译
有两个编译器入口点@angular/compiler-cli
:ngtsc
和ngcc
。
ngtsc
它是TypeScript 到 JavaScript 的转换器,将 Angular 装饰器具体化为静态属性。其工作方式与启用 Ivy 时ngc
类似。ngtsc
ngcc
Angular 兼容性编译器处理来自node_modules
文件夹的 NPM 库代码,生成与 Ivy 兼容的等效库代码。ngcc
也可以由 Webpack 等代码加载器运行,以按需编译包,而不是在node_module
包文件夹中编写。
ngc
打开package.json
并添加ngc
脚本:
"scripts": {
"ng": "ng",
...
"ngc": "ngc"
}
为了获得这些文件,请tsconfig.json
运行:"declaration": true,
.d.ts
$ npm run ngc
组件编译的结果位于dist\out-tsc\src
。该Welcome to Angular!
组件被翻译成:
import { Component, Input } from '@angular/core';
import * as i0 from "@angular/core";
export class AppComponent {
constructor() {
this.title = 'Angular';
}
}
AppComponent.ɵfac = function AppComponent_Factory(t) { return new (t || AppComponent)(); };
AppComponent.ɵcmp = i0.ɵɵdefineComponent({ type: AppComponent, selectors: [["app-root"]], inputs: { title: "title" }, decls: 3, vars: 1, consts: [[2, "text-align", "center"]], template: function AppComponent_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "div", 0);
i0.ɵɵelementStart(1, "h1");
i0.ɵɵtext(2);
i0.ɵɵelementEnd();
i0.ɵɵelementEnd();
} if (rf & 2) {
i0.ɵɵadvance(2);
i0.ɵɵtextInterpolate1(" Welcome to ", ctx.title, " ");
} }, styles: [""] });
/*@__PURE__*/ (function () { i0.ɵsetClassMetadata(AppComponent, [{
type: Component,
args: [{
selector: 'app-root',
template: `
<div style="text-align:center">
<h1>
Welcome to {{ title }}
</h1>
</div>
`,
styleUrls: ['./app.component.css']
}]
}], null, { title: [{
type: Input
}] }); })();
然后是app.component.d.ts
定义文件:
import * as i0 from "@angular/core";
export declare class AppComponent {
title: string;
static ɵfac: i0.ɵɵFactoryDef<AppComponent>;
static ɵcmp: i0.ɵɵComponentDefWithMeta<AppComponent, "app-root", never, { "title": "title"; }, {}, never>;
}
注意:
默认情况下,Ivy 是启用的,如果禁用它,您将获得不同的编译输出。
JIT 与 AoT
编译器可以在JiT(即时)模式下工作,它会随应用程序一起交付,并在运行时进行编译。而AoT(提前)编译则会在构建时编译所有内容,从而提高应用程序的运行速度,并且无需将编译器随应用程序一起交付。
使用 Angular 9,由于 Ivy,编译速度更快,并且默认情况下"aot": true
。
编译模型
TypeScript 和 Angular 编译器都保留了重要的元数据,这些元数据不能包含在生成的.js
文件中。Angular.d.ts
使用框架特有的元数据来增强这些文件,以便重用,从而更好地进行组件类型检查。
TypeScript 编译模型
JavaScript 代码没有类型信息,它只是准备在浏览器中执行。TypeScript 编译器会转换所有.ts
源文件,但类型信息并没有完全丢失,定义文件有助于保留接口以供将来使用。
例如该文件library.ts
被编译成library.js
并library.d.ts
生成,它描述了库的接口{: .italic-red-text }或公共API {: .italic-red-text }。
定义文件包含类型信息{: .italic-red-text },以帮助 TypeScript 对库的使用情况进行静态类型检查。例如,当应用程序声明 npm 依赖库时,可以导入以下功能:
import {AwesomeLib} from 'awesome-lib';
// use the lib
app.js
TypeScript 将在利用定义文件中对库的使用进行静态类型检查library.d.ts
:
declare class AwesomeLib {
awesomeMethod(): string;
}
Angular Ivy 编译模型
Angular 组件在装饰器中声明了一些有用的信息,例如selector
。这些信息对于在其他地方使用组件以及代码的静态类型检查非常重要。
Angular Ivy 编译器已得到改进,不再浪费这些信息,并丰富了定义文件的公共接口。例如,以下 Angular 组件:
@Component({
selector: 'awesome-comp'
})
export class AwesomeComponent {
@Input() value: string;
}
将被编译成 JavaScript,并且元数据将丰富定义文件,如下所示:
export declare class AwesomeComponent() {
value: string;
static ngComponentDef: ng.ComponentDef<
AwesomeComponent,
`awesome-comp`,
{value: 'value'}
>;
}
定义文件成为了组件的公共 API {: .italic-red-text }。编译器可以利用它来对使用它的代码进行类型检查:
<awesome-comp [value]="a value">
just a component
</awesome-comp>
TypeScript 转译器架构
下图(感谢 Angular 团队)展示了正常流程以及将一个文件tsc
转换为一个文件的步骤。.ts
.js
|------------|
|----------------------------------> | TypeScript |
| | .d.ts |
| |------------|
|
|------------| |-----| |-----| |------------|
| TypeScript | -parse-> | AST | ->transform-> | AST | ->print-> | JavaScript |
| source | | |-----| | |-----| | source |
|------------| | | | |------------|
| type-check |
| | |
| v |
| |--------| |
|--> | errors | <---|
|--------|
- 解析:递归下降解析器,生成抽象语法树(AST);
- 类型检查:对每个文件执行类型分析,报告发现的错误。未被修改
ngtsc
; - AST 到 AST:删除类型声明,将类转换为 ES5、
async
方法等。
扩展点
TypeScripttsc
提供了一些扩展点来改变其输出:
CompilerHost.getSourceFile
修改源代码;CustomTransformers
改变变换列表;WriteFileCallback
在写入之前拦截输出。
编译步骤
这ngtsc
是 TypeScript 编译器的包装器tsc
,它扩展并修改了正常的编译流程。
提示:
TypeScript 转译器无法编译 Angular 模板和装饰器,因此 Angular 编译器会介入,将Angular 装饰器具体化为静态属性。完成后,TypeScript 编译器可以继续生成 JavaScript 代码。换句话说,Angular 编译器允许用 Angular 声明式语法编写的代码参与 TypeScript 编译过程。
1. 程序创建
从文件开始tsconfig.json
,TypeScript 进程通过import
语句发现应用程序源文件。
2.分析
Angular 编译器会获取所有.ts
收集到的文件,并逐个类地查找 Angular 声明式语法代码,基本上就是 Angular 的东西。例如,编译器会收集组件的隔离信息,但不会收集模块的隔离信息。记住,@Component
由于组件模板中使用的组件模块模板导入的导出是传递闭包解析,因此编译器需要了解模块的全局信息。
3. 解决
Angular 编译器会再次审视整个应用程序,但这次会从更宏观的角度审视,包括模块。现在,所有代码都可以通过 TypeScript 编译器的下一步理解和解析。优化将在此步骤中进行。
4. 类型检查
TypeScript 检查应用程序中的错误,包括模板,现在是一系列命令式指令。
5. 发射
编译过程中最耗时的操作是将 TypeScript 代码转换为可供浏览器运行的 JavaScript。现在,Angular 组件类仅使用命令式代码来描述模板的外观。
编译器功能
Angular 编译器有很多有趣的特性,其中一些特性在新的 Angular Ivy 架构中得到了增强和改进。让我们来看看其中的一些。
模块编译范围
模块作用域允许编译器唯一地解析应用程序所使用的 Angular 组件。考虑以下应用程序模块:
@NgModule({
declarations: [
AppComponent,
HelloComponent
]
})
export class AppModule {}
module 装饰器属性declarations
是模块编译范围的,它保存了应用程序模板中使用的 Angular 组件数组。该HelloComponent
组件来自一个库,它拥有自己的定义文件,并且如前所述,其中包含丰富的元数据。
开发者在数组中声明愿意使用该组件,然后可以在其中一个应用模板中添加相应的选择器来使用该组件。
然后,编译器可以唯一地匹配组件,并验证选择器及其属性的使用。
导出编译范围
模块可用于导出 Angular 组件,作为一种让外部世界可见某些已实现组件的方法。
提示:
模块模式在计算机语言中非常常用,它是一种将复杂应用程序拆分成可在其他地方复用的小块的方法。想想 ESM 或 CommonJS 等。Angular 也提供了一种通过模块概念创建模块化应用程序的方法。模块还用于隐藏实现细节并选择哪些内容应为public。exports
属性是选择将 Angular 组件设置为 public 以便其他组件可复用的方式。
以下示例HelloModule
声明并导出了HelloWorld
组件。模块库仅实现一个组件,并将其导出给所有想要使用它的应用程序:
@NgModule({
declaration: [HelloComponent],
exports: [HelloComponent]
})
export class HelloModule {}
当应用程序想要使用该HelloWorld
组件时,只需声明它:
@NgModule({
declarations: [
AppComponent,
HelloComponent
]
})
export class AppModule {}
然后编译器可以:
- 唯一地找到组件定义;
- 从模板中找出所使用的组件;
- 生成代码并相应地引用它;
- 帮助 tree-shaker 删除未引用的内容。
部分评估
编译器实际上尝试在装饰器中运行 TypeScript 代码
Alex Rickabaugh - Angular Connect 2019
Angular 编译器几乎包含一个完整的TypeScript 解释器。以下示例中:
import {SOME_MODULES} from './some_module';
@NgModule({
declarations: [HelloComponent, ByeComponent],
exports: [HelloComponent, ByeComponent],
imports: [...SOME_MODULES]
})
export class AnotherModuel {...}
编译器可以跟踪并评估导入引用。由于编译时信息不足,某些评估只是部分的。
例如:
import {SOME_MODULES} from './some_module';
@NgModule({
imports: [SOME_MODULES.modules]
})
export const SOME_MODULES = {
modules: [HelloModule, ByeModule],
// not available at compile time
viewportSize: {
x: document.body.scrollWidth,
y: document.body.scrollHeight
}
}
只有在运行时才有一些值可用。这些值是动态表达式。编译器看到的前一个对象大致如下:
SOME_MODULES: {
"modules": [
Reference(HelloModule),
Reference(ByeModule)
],
"viewportSize": {
x: DynamicValue(document.body.scrollWidth),
y: DynamicValue(document.body.scrollHeight)
}
}
在哪里:
SOME_MODULES
是一个具有两个属性的对象;DynamicValue
是“无法通过”的指标。
编译不会停止,它会继续下去,因为这种情况可能相当常见。
import {SOME_MODULES} from './some_module'
@Component({
styles: [`
:host {
width: $SOME_MODULES.viewportSize.x
}
`]
})
虽然属性的编译仍在继续modules
,但viewportSize
由于无法确定其值,因此无法进行编译。系统会生成一条关于样式的详细说明性错误消息。
模板类型检查
模板和表达式
<account-view
[account]="getAccount(user.id, 'primary') | async">
</account-view
它们被编译成 TypeScript 代码,成为类型检查块。因此,块被发送到 TypeScript 编译器,然后在模板上下文中返回可能的错误。
function typeCheckBlock(ctx: AppComponent) {
let cmp!: AccountViewCmp;
let pipe!: ng.AsyncPipe;
cmp.account /*273,356*/ = (pipe.transform(
ctx.getAccount((ctx.user /*311,315*/).id /*311,318*/,
"primary" /*320,329*/)
/*300,331*/)
/*300,338*/) /*289,339*/;
}
如何在模板上下文中返回错误?好吧,代码被翻译成额外的内容offset comments
,这样就可以实现上下文化。
例子
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<div style="text-align:center">
<h1>
Welcome to {{ title }}!
<tr *ngFor="let hero of heroes">
<td>{{hero.name}}</td>
</tr>
</h1>
</div>
`,
styleUrls: []
})
export class AppComponent {
@Input() title = 'Angular';
heroes = 'fake array';
}
启用 Ivy 的Angular 9保留了该标志的行为,并引入了超越 Angular 8 类型检查器fullTemplateTypeCheck
的严格模式。在以下位置激活严格模式:strictTemplates
tsconfig.json
...
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictTemplates": true,
...
}
重新运行npm run ngc
并获取编译器报告的完整更好的错误:
src/app/app.component.html:7:13 - error NG2339: Property 'name' does not exist on type 'string'.
7 <td>{{hero.name}}</td>
~~~~~~~~~
src/app/app.component.ts:5:16
5 templateUrl: './app.component.html',
~~~~~~~~~~~~~~~~~~~~~~
Error occurs in the template of component AppComponent.
模板类型检查页面上有更多详细信息。
结论
Angular 9 Ivy 渲染架构引入了新的编译器和新的渲染引擎,不仅可以利用增量 DOM 技术,还可以利用更强大的编译器。
Ivy 默认启用,--aot
这是默认的开发方式,因为新编译器比旧编译器速度更快。默认启用 AoT 模式还可以降低开发代码与生产代码之间出现差异的风险。
Ivy 编译器更进一步,它具有更好的类型检查,使得报告的错误更加明确,开发人员更容易识别问题的根本原因。
我们都期待看到新的编译器应用于 Angular Elements。
参考
- 编译器架构(Ivy)
- Angular Ivy 的理论,Alex Richabaugh
- Angular Ivy 示例
- 深入探究 Angular 编译器,作者:Alex Richabaugh
- 深度编译器
- 深入编译器视频
- Angular 中的 Ivy 引擎