理解 Angular Ivy 库编译
原文链接:https://blog.lacolaco.net/2021/02/angular-ivy-library-compilation-design-in-depth-en/
本文将详细介绍如何使用 Ivy 编译 Angular 库(Angular v11.1 版本新增了此功能)。本文的目标读者是开发 Angular 第三方库的开发者,以及对 Angular 内部机制感兴趣的人士。即使您不了解本文内容,也无需掌握 Angular 应用开发的相关知识。
本文内容基于 Angular 团队编写的设计文档。
如何使用 Ivy 编译库
src/tsconfig.lib.prod.json当您使用 Angular CLI 或类似工具开发 Angular 库时,Ivy 目前在生产版本中处于禁用状态。这可能是在类似如下的文件中设置的。
{
"angularCompilerOptions": {
"enableIvy": false
}
}
即使应用程序未启用 Ivy,使用此配置编译并发布到 NPM 的 Angular 库仍然可以兼容使用。
从 Angular v11.1 开始,您可以尝试移除对尚未启用 Ivy 的应用程序的兼容性,并编译针对已启用 Ivy 的应用程序优化的库。要对发布到 NPM 的库使用 Ivy 编译,请按如下方式配置:
{
"angularCompilerOptions": {
"enableIvy": true,
"compilationMode": "partial"
}
}
"compilationMode": "partial"这是一个重要的部分,我将在本文后面解释它的含义。当然,使用此设置编译的库只能在启用 Ivy 的应用程序中使用,因此目前仍不建议这样做。
顺便一提,对于仅在单体仓库中本地使用的库,例如 Angular CLI 和 Nrwl/Nx,您可以直接使用 ` enableIvy: true.`。"compilationMode": "partial"只有发布到 NPM 的库才需要使用 `.`。本文稍后也会解释这种区别。
{
"angularCompilerOptions": {
"enableIvy": true
}
}
术语
为了使接下来的解释简洁明了,我们先来理清一下术语。
| 学期 | 意义 |
|---|---|
| Angular 装饰器 | Angular 定义的装饰器,例如@Component,@Directive和@Injectable。 |
| 编译器 | Angular 编译器是一个分析 Angular 装饰器并生成可执行代码的工具。 |
ngc |
Angular 编译器的可执行 CLI |
| Ivy编译器 | Angular v9 中引入的编译器 |
| View Engine (VE) 编译器 | Angular v8 之前默认使用的已弃用编译器 |
Ivy 编译应用程序
在开始讨论库之前,我们先来编译一个默认启用 Ivy 的应用程序。编译器会分析应用程序中的 Angular 装饰器,并根据提取的元数据生成可执行代码。
我们来看一个编译简单组件的例子。假设我们有以下组件。
@Component([
selector: 'some-comp',
template: `<div> Hello! </div>`
})
export class SomeComponent {}
如果使用 Ivy 编译这段代码,将会得到以下 JavaScript 输出。这两点如下:
- 装饰器不会保留在 JavaScript 代码中。
- 生成的代码将作为静态字段插入到组件类中。
export class SomeComponent {}
SomeComponent.ɵcmp = ɵɵdefineComponent({
selectors: [['some-comp']],
template: (rf) => {
if (rf & 1) {
ɵɵelementStart('div');
ɵɵtext(' Hello! ');
ɵɵelementEnd();
}
},
});
Ivy 编译器会根据装饰器中包含的元数据生成用于创建定义的代码。HTML 模板(原本是一个字符串)会变成可执行代码,即模板函数。模板函数中使用的 ` <template> ɵɵelementStart` 和`<template>` 被称为模板指令,它们抽象了具体的 DOM API 调用和数据绑定更新过程。ɵɵtext
Angular 编译在内部分为两个步骤:分析步骤和代码生成步骤。
分析步骤
在编译的分析步骤中,它会整合从整个应用程序的装饰器中获取的元数据,并检测组件/指令之间的依赖关系。此时,关键在于 `<script>` 标签@NgModule。它用于确定模板中包含的未知 HTML 标签和属性对应的引用。分析步骤完成后,编译器会获得以下信息。
- 哪些组件依赖于哪些指令/组件?
- 实例化每个组件/指令需要哪些依赖项?
代码生成步骤
在代码生成步骤中,它会根据分析步骤中获得的信息,为每个 Angular 装饰器生成代码。生成的代码有两个要求:本地性和运行时兼容性。
地点
局部性也体现在自包含性上。这意味着编译组件所需的所有引用都包含在组件类本身中。这使得差异化构建更加高效。为了便于理解,让我们回顾一下 Ivy View Engine 之前没有局部性时遇到的问题。
VE编译器生成的代码文件*.ngfactory.js独立于原始文件。Angular*.ngfactory.js在运行时执行该文件,生成的代码引用了原始组件类。当一个组件依赖于另一个组件时,这种方法就会出现问题。
例如,当一个组件<app-parent>使用模板调用另一个组件时<app-child>,模板中不存在parent.component.ts对child.component.ts另一个组件的 JavaScript 模块引用。这种父子依赖关系仅存在于模板parent.component.ngfactory.js和组件之间child.component.ngfactory.js。
由于直接编译结果parent.component.js既不参考 `<pre>`child.component.js也不参考 `<pre> child.component.ngfactory.js`,因此无法确定何时需要重新编译。所以,ViewEngine 每次构建时都必须重新编译整个应用程序。
为了解决这个问题,Ivy 编译器会将代码生成为类的静态字段。在生成的代码中,模板中引用的指令类也会被包含在内。这样就很容易确定当某个文件发生更改时,哪些文件会受到影响。
ParentComponent如您所见,使用 Locality 进行代码生成时,只有当代码本身ChildComponent发生更改时才需要重新编译。
// parent.component.js
import { ChildComponent } from './child.component';
ParentComponent.ɵcmp = ɵɵdefineComponent({
...
template: function ParentComponent_Template(rf, ctx) {
if (rf & 1) {
ɵɵelement(2, "app-child");
}
},
// Directives depended on by the template
directives: [ChildComponent]
});
运行时兼容性
代码生成中另一个重要因素是运行时兼容性。编译应用程序时这并非问题,但编译库时却至关重要。
在应用程序中,编译器版本和 Angular 运行时版本基本匹配,因为编译是在应用程序构建过程中同时完成的。但是,对于库来说情况并非如此。
对于发布到 NPM 的库,必须考虑到编译该库的 Angular 版本与运行时使用该库的应用程序所使用的 Angular 版本可能不匹配。这里的一个重要问题是生成代码中调用的 Angular API 的兼容性。编译时版本中存在的 API 可能在运行时版本的 Angular 中不存在,或者它们的签名可能已经更改。因此,代码生成的规则必须根据执行代码的运行时环境所使用的 Angular 版本来确定。
在 monorepo 中本地使用的库是 Ivy 可编译的,因为只要它在 monorepo 中,就可以确保库和应用程序具有相同的 Angular 版本。
图书馆汇编
这就是主要内容。首先,我们来看看如何使用 Ivy 编译库enableIvy: false,这是 v11 版本目前推荐的设置。不使用 Ivy 编译库只是将分析步骤中收集的元数据内联。Angular 装饰器元数据嵌入到 static 字段中,如下所示。
库编译会将元数据转换为可发布到 NPM 的 JavaScript 表示形式。然而,这仍然是元数据,加载到应用程序后无法作为组件执行。它需要基于此元数据再次编译。Angular Compatibility Compiler就是ngcc完成此操作的工具。
ngcc
由于我们无法确定应用程序端的编译器是 Ivy 还是 VE,因此保持兼容性的唯一方法是在应用程序端编译库代码。这就是为什么ngcc它会在应用程序构建时运行的原因。
编译结果ngcc与直接编译库的结果相同。区别在于,前者ngc使用 TypeScript 中的装饰器作为元数据,而后者ngcc使用.decoratorsJavaScript 中的装饰器作为元数据。
虽然ngcc实现了让库能够兼容地发布到 NPM 的目的,但频繁的编译却破坏了开发者的体验。许多开发者可能都体会过ngcc每次安装库后都要重复运行编译命令的烦恼。该命令ngcc会覆盖从 NPM 安装的库代码并进行编译,因此如果该命令更改了node_modules库文件的内容,则必须重新编译。node_modulesnpm install
但最初,ngcc这只是一个临时方案,直到从应用程序中移除 View Engine 支持为止。下文将介绍的 Ivy 库编译器是一种新的 Ivy 原生库编译机制,它解决了之前阐述的问题ngcc。
常春藤图书馆汇编
最大的问题在于ngcc应用程序端的编译执行成本。如果ngcc速度足够快,我们可以在应用程序编译时同时编译库,而无需将编译结果持久化node_modules。但由于执行成本很高,我们希望减少编译次数并保存结果。
另一方面,如果我们在发布库之前完成编译,虽然可以加快应用程序的构建速度,但会损失运行时兼容性。代码生成步骤确实需要在应用程序的 Angular 版本中完成。
因此,Ivy库编译的概念是一套机制,它包括库安装后快速运行代码生成步骤的机制,以及NPM发布前完成分析步骤的机制。第一个机制称为库链接,第二个机制称为链接时优化(LTO)编译。
LTO 汇编(预发行汇编)
LTO 编译是在发布到 NPM 之前进行的,它是一种仅完成整个编译过程的分析步骤并将结果嵌入 JavaScript 的机制。如引言中所述,"compilationMode": "partial"启用此设置后,编译器将对库执行 LTO 编译。
{
"angularCompilerOptions": {
"enableIvy": true,
"compilationMode": "partial"
}
}
编译后的 JavaScript 代码如下。它看起来与正常的编译结果类似,但重要的是模板被保留为字符串,并且具有Locality 属性。
分析步骤中获取的信息以声明的形式内联。它包含所依赖的指令列表,并且具有局部性,使其仅使用文件中的信息即可执行代码生成步骤。通过将模板函数的代码生成延迟到链接之后,该库可以确保运行时兼容性。
此外,还包含了 Angular 版本的 LTO 编译。即使模板相同,也可以根据编写模板的 Angular 版本和运行时版本在链接时进行优化。
链接库
安装 LTO 编译库的应用程序会在构建时进行即时链接。链接器会根据 LTO 编译中的声明生成代码,并将其替换为应用程序可以使用的定义。
与需要分析步骤的链接方式不同ngcc,由于 LTO 编译的局部性,链接过程可以针对每个文件独立执行,因此它可以像 webpack 一样作为模块解析的插件运行。在 Angular CLI 构建中,它被实现为一个名为 `Babel_plugin` 的 Babel 插件AngularLinker。
包起来
新版 Ivy 库的编译过程可以概括如下:
- 库编译分为两个部分:NPM 发布之前和发布之后。
- 其中一个是LTO 编译过程,该过程在发布到 NPM 之前完成装饰器分析。
- 另一个过程是链接过程,它通过在应用程序构建时生成代码来完成库的编译。
我希望这篇文章能帮助读者了解新的 Ivy 库编译是如何设计的,它基于应用程序和库在编译方面的差异,以及ngcc目前使用中存在的问题。





