Angular Ivy:详细介绍
目录
Angular Ivy 是Angular 9 版本默认提供的新渲染架构。Angular渲染架构并不是一次彻底的改造,Angular 2.0、Angular 4.0 以及现在的 Angular 9.0 都引入了新的编译器和运行时引擎。
目前,Angular 稳定版本为 8.2.14,Angular 9 为 RC5。
免责声明:
本文包含对 Angular 工作原理的初步探究,包括阅读部分源代码、调试一个简单应用程序以及了解编译器的工作原理。部分术语或定义可能存在错误。
幻灯片
这篇文章附带一个用markdown 编写的演示文稿reveal.js
,通过 GitHub 呈现并可在 GitHub 上获取。
行话
- 渲染架构:允许执行 Angular 应用程序的编译器和运行时引擎管道。
- 运行时渲染函数集/指令集:运行时、模板、装饰器可理解的JavaScript函数集,被转换成一系列指令。
- 虚拟 DOM 和增量 DOM:在 DOM 中创建和更新组件的技术。
- 视图引擎: Angular 4 中引入的渲染架构,
angular.json
是工作区或构建配置文件。tsconfig.app.json
是项目配置文件。.ngfactory.js
装饰器工厂文件的后缀,类装饰器之类的@Component
被编译器翻译成外部文件。- 局部性:编译器应该只使用来自组件装饰器及其类的信息。
- 全局编译:编译过程需要全局静态分析来发出应用程序代码。
渲染架构
什么是渲染架构?它是“编译器:运行时”对。Angular 框架由两个主要部分组成:
- 编译器将用 Angular 声明语法编写的模板转换为富含变化检测的 JavaScript 指令;
- 运行时执行编译器生成的应用程序代码。
目前,Angular 8 使用称为View Engine 的渲染架构:
- 视图引擎已在 Angular 版本 4 中引入,并在版本 8 中仍在使用,但已发现一些限制
- 不可摇树优化:应用
Hello World
程序和非常复杂的应用程序都由相同的完整运行时执行。例如,如果不使用国际化模块,它无论如何都是运行时的一部分,基本上运行时就无法摇树优化; - 没有增量编译: Angular 编译是全局的,它不仅涉及应用程序,还涉及库。
- 不可摇树优化:应用
- Ivy将从版本 9 开始成为新的默认渲染引擎,并解决视图引擎当前的问题:
- 简化Angular 内部的工作方式;
- 可摇树的应用
Hello World
程序不需要完整的 Angular 运行时,并且仅捆绑在 4.7 KB 中; - 增量编译是不可能的,因此编译比以往更快,
--aot
现在甚至可以在开发模式下使用(来自 Angular 团队的建议)。
常春藤是一个推动者。
Igor Minar - Angular 团队
增量DOM是新渲染引擎的基础。
增量 DOM 与虚拟 DOM
每个组件都会被编译成一系列指令。这些指令会创建 DOM 树,并在数据发生变化时进行更新。
维克托·萨夫金- Nrwl
每个编译的组件都有两组主要指令:
- 组件首次渲染时执行的视图创建指令;
- 变更检测指令,当组件发生变化时更新 DOM。
变更检测本质上是一组在编译时添加的指令。由于开发人员只需关注Angular 模板声明中的变量绑定,因此他们的工作变得更加轻松。
增量 DOM 可以实现更好的捆绑包大小和内存占用,从而使应用程序能够在移动设备上表现良好。
虚拟 DOM
React 和 Vue 都基于虚拟 DOM的概念来创建组件并在检测到变化时重新渲染它。
渲染 DOM 是一项非常耗时的操作。当组件添加到 DOM 或发生更改时,必须进行重绘操作。虚拟 DOM 策略旨在减少真实 DOM 上的工作量,从而减少用户界面需要重绘的次数。
提示:
终端用户有时并未意识到用户界面渲染背后的复杂性。一个简单的点击就可能引发 HTTP 请求、组件的变更、其他组件的变更等等。对于用户来说,单个变更可能意味着一系列必须应用于 DOM 的复杂变更。
每次在 DOM 中添加、移除或更改新组件时,都会发生 DOM 操作。因此,它不是直接操作 DOM,而是操作一个称为虚拟 DOM 的 JSON 对象。添加新组件或移除现有组件时,会创建一个新的虚拟 DOM,添加或移除节点,并计算虚拟 DOM 之间的差异。一系列转换操作会应用于真实 DOM。
React 文档建议使用 JSX( JavaScript 的语法扩展)来定义React 元素。JSX 不是模板语言。模板是一种丰富的 JavaScript 表达式,在运行时进行解释。您也可以使用纯 JavaScript 来代替 JSX。
虚拟 DOM 技术有一些缺点:
- 每次发生变化(添加或删除节点)时都会创建一整棵树,因此内存占用非常重要;
- 只要diff算法能够计算虚拟 DOM 之间的差异,就需要一个解释器。在编译时,我们并不知道渲染应用程序需要哪些功能,因此必须将整个过程交付给浏览器。
增量 DOM
它是新渲染引擎的基础。每个组件模板都会被编译成创建和变更检测指令:一个用于将组件添加到 DOM,另一个用于就地更新 DOM 。
指令不由Angular 运行时(渲染引擎)解释,但指令就是渲染引擎。
维克托·萨夫金- Nrwl
由于运行时不会解释模板组件的指令,而是解释组件引用的指令,因此很容易删除那些未被引用的指令。在编译时,可以将未使用的指令从 bundle 中排除。
渲染 DOM 所需的内存量与组件的大小成正比。
提示:
编译后的模板组件引用了保存实现的 Angular 运行时的一些指令。
启用 Angular Ivy
可以在具有最新 Angular 版本的现有项目中启用 Ivy,也可以直接使用 Ivy 搭建项目。
在现有项目中启用 Ivy
运行现有的Angular(8.1.x)项目:
$ ng update @angular/cli@next @angular/core@next
Angular 核心和 CLI 都将在最新的候选版本中更新。需要注意的是工作区配置文件"aot": true
中的以下内容:angular.json
使用 Ivy 进行 AOT 编译速度更快,应该默认使用。
然后在中添加角度编译器选项tsconfig.app.json
:
{
"compilerOptions": { ... },
"angularCompilerOptions": {
"enableIvy": true
}
}
与 Ivy 合作的新项目
要使用 Ivy 运行启动新项目:
$ new my-app --enable-ivy
禁用 Ivy
要禁用 Ivy:
- 在
angular.json
集合中"aot": false
; - 删除
tsconfig.app.json
选项angularCompilerOptions
或设置"enableIvy": false
。
Angular Ivy 编译
考虑以下Hello World Angular 组件:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<div style="text-align:center">
<h1>
Welcome to {{ title }}!
</h1>
</div>
`,
styleUrls: []
})
export class AppComponent {
@Input() title = 'Angular!';
}
在启用 Ivy 的 Angular 8 中,它会被编译成以下代码:
class AppComponent {
constructor() {
this.title = 'Angular!';
}
}
AppComponent.ngComponentDef = defineComponent({
selectors: [['app-root']],
factory: function() { return new AppComponent();}
},
template: function(flags, context) {
if (flags & 1) {
elementStart(0, "div", 0);
elementStart(1, "h1");
text(2);
elementEnd();
elementEnd();
} if (flags & 2) {...}
},
directives: [...]
});
在 Angular 8 中使用 Ivy 时,Angular 装饰器会被编译成被装饰类中的静态字段@Component
。因此,它变成了ngComponentDef
静态字段。回到视图引擎,ngc
编译器会为每个组件和模块生成.ngfactory
单独的文件。使用 Ivy 时,编译器生成的代码会被移到组件类的静态字段中。
、elementStart()
、elementEnd()
等是组件引用的说明,每个组件都是自己的工厂,框架不会解释组件。
所有在编译时未引用的指令都将从最终的应用程序包中删除。
提示:
View Engine 运行时是一个单一的单体解释器,不支持 tree-shaking,必须完全迁移到浏览器。与之不同的是,Angular Ivy 运行时是一个指令集,它是一组渲染函数,类似于模板的汇编语言。
在 Angular 9 RC5 和 Ivy 中,编译稍有不同:
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, "! ");
} }, encapsulation: 2 });
Angular Ivy 的功能
Angular Ivy 是一个推动者。它简化了 Angular 的内部工作方式和编译过程,解决了当前视图引擎的限制,并使 Angular 能够轻松扩展新功能。
新的 Ivy 工程由三个主要目标驱动:摇树、局部性和灵活性。
摇树
树摇动是从包中删除死代码的操作,因此如果应用程序不引用某些运行时渲染函数,则可以从包中省略它们,从而使包变得更小。
死代码来自库,包括 Angular。Angular CLI 由以下工具提供支持:
Webpack uglify 插件Webpack Terser 插件作为 tree-shaker,反过来,它会从Angular Build Optimizer 插件接收关于哪些代码被使用、哪些代码未被使用的信息。Angular 编译器不会发出这些指令,但该插件可以收集组件引用指令的信息,从而指示
丑化更简洁地说明在捆绑包中包含/排除的内容。
虽然@angular/core
框架是可摇树的,但视图引擎运行时却不是,它不能分解成小块,这主要是由于静态Map<Component, ComponentFactory>
变量。
增量编译
Angular 8 编译管道ng build prod --aot
由五个阶段组成,其中tsc
和ngc
生成模板工厂。ngc
此外,它还会编译库。Ivy 支持增量编译,也就是说,库可以在 npm 上编译和部署。
位置
目前 Angular 依赖于全局编译。编译过程需要对整个应用程序进行全局静态分析,以组合不同的编译输出(应用程序、来自 monorepo 的库以及来自 npm 的库),然后再生成 bundle。此外,将 AOT 库组合到 JIT 应用程序中非常复杂。
提示:
编译器应该只使用组件装饰器及其类提供的信息,而不使用任何其他信息。这简化了整个编译过程,无需component.metadata.json
在component.ngfactory.json
编译流水线中进行复杂的管理。
局部性是一种规则。Ivy 编译引入了组件/指令公共 API 的概念: Angular 应用程序可以安全地引用组件和指令公共 API,而不再需要了解太多依赖关系,因为组件文件中添加了额外的信息.d.ts
。
示例:Ivy 库编译
将库添加到运行应用程序的 monorepo ng generate library mylib
。
使用 编译该库ng build mylib
,将生成以下文件:
├── bundles
├── ...
├── lib
│ ├── mylib.component.d.ts
│ ├── mylib.module.d.ts
│ └── mylib.service.d.ts
├── mylib.d.ts
├── package.json
└── public-api.d.ts
还要注意,由于 Ivy 激活,版本 9 中会显示以下新消息:
Building Angular Package
******************************************************************************
It is not recommended to publish Ivy libraries to NPM repositories.
Read more here: https://next.angular.io/guide/ivy#maintaining-library-compatibility
******************************************************************************
生成的组件
这是 Angular CLI 生成的组件:
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'lib-mylib',
template: `
<p>mylib works!</p>
`,
styles: []
})
export class MylibComponent implements OnInit {
constructor() { }
ngOnInit() { }
}
编译库代码
mylib.metadata.json
不再生成元数据文件,元数据现在是定义文件的一部分。
组件的定义文件:
import { OnInit } from "@angular/core";
import * as i0 from "@angular/core";
export declare class MylibComponent implements OnInit {
constructor();
ngOnInit(): void;
static ɵfac: i0.ɵɵFactoryDef<MylibComponent>;
static ɵcmp: i0.ɵɵComponentDefWithMeta<MylibComponent,"lib-mylib",never,{},{},never>;
}
模块的定义文件:
import * as i0 from "@angular/core";
import * as i1 from "./mylib.component";
export declare class MylibModule {
static ɵmod: i0.ɵɵNgModuleDefWithMeta<MylibModule, [typeof i1.MylibComponent], never, [typeof i1.MylibComponent]>;
static ɵinj: i0.ɵɵInjectorDef<MylibModule>;
}
以及服务的定义文件:
import * as i0 from "@angular/core";
export declare class MylibService {
constructor();
static ɵfac: i0.ɵɵFactoryDef<MylibService>;
static ɵprov: i0.ɵɵInjectableDef<MylibService>;
}
向组件添加属性
向库组件添加一个输入字段:
@Component({
selector: 'lib-mylib',
template: `
<p>Please input your phone</p>
<input #phone placeholder="phone number" />
`,
styles: []
})
export class MylibComponent implements OnInit {
@Input('phone-number') private phone: string;
constructor() { }
ngOnInit() {
}
}
该别名phone-number
将被添加到输入属性,为公共 API 提供额外的元数据。编译器会生成以下定义文件:
import { OnInit } from '@angular/core';
import * as i0 from "@angular/core";
export declare class MylibComponent implements OnInit {
private phone;
constructor();
ngOnInit(): void;
static ɵfac: i0.ɵɵFactoryDef<MylibComponent>;
static ɵcmp: i0.ɵɵComponentDefWithMeta<MylibComponent, "lib-mylib", never, { 'phone': "phone-number" }, {}, never>;
}
一个装饰器,将类字段标记为输入属性,并提供配置元数据。输入属性会绑定到模板中的 DOM 属性。在变更检测期间,Angular 会自动用该 DOM 属性的值更新数据属性。
输入装饰器- Angular 文档
属性phone-number
是公共 API 的名称部分,而phone
是私有名称,属于实现细节。由于属性名称可能会发生变化,因此每次都必须编译代码,以便在属性名称不匹配时发出错误。因此,当前的 Angular 版本必须依赖于全局编译。
Angular Ivy 依赖于公共 API,因此可以编译库代码并安全地运送到 npm。
浏览器属性
输入属性绑定到模板中的 DOM 属性。
基本上
当浏览器加载页面时,它会“读取”(或者说“解析”)HTML并从中生成DOM对象。对于元素节点,大多数标准HTML属性会自动成为DOM对象的属性。
属性和特性- javascript.info
Angular 编译器将装饰器和模板转换为 JavaScript 指令,不仅可以在 DOM 中创建元素,还可以创建运行时用来“保持”应用程序活动的额外内容属性和特性。
灵活性
Angular Ivy 比 View Engine 更灵活,因为如果Angular 引入了新的功能,它会在指令集中实现新的指令。Ivy 更容易扩展和优化。
Angular Ivy 构建管道
Angular 应用程序的编译只是整个过程的一半,因为应用程序所依赖的库必须与新的运行时兼容。
ngcc
(Angular 兼容编译器)是一种新的编译器,用于转换和编译库。与AngularViewEngine
之前的渲染引擎兼容的库将被转换为 Ivy 指令,以便“库可以参与 Ivy 运行时”并实现完全兼容。
新的编译器已经实现,使得库与新格式兼容,而无需维护人员重写其中的重要部分,而且,并非所有应用程序都需要与 Ivy 兼容。
在 Angular 9 版本中,Ivy 仅适用于应用程序,ngcc
用于转换现有库使其兼容 Ivy。随着时间的推移,应用程序将越来越兼容 Ivy,因此这些库ngcc
将不再必要。在构建或安装过程中,可以动态地将库转换为兼容 Ivy 的库。
从版本 9 到版本 11 的增量过渡ngcc
仅在少数情况下是必要的:
角度版本 | ngcc |
---|---|
9 | Ivy 上的应用程序(选择退出)和 VE 兼容库 |
10 | 稳定 Ivy 指令集,库发布 Ivy 代码 |
11 | ngcc 备份过时的库或尚未更新的库 |
ngcc-validation
项目是 Angular 团队测试库兼容性的方式。
组件延迟加载功能
Angular 是一个推动者,它不仅能提升构建的性能,还能提升应用程序的性能。从 2.0 版本开始,Angular 就增加了组件延迟加载功能,但仅限于路由器级别。组件级别的延迟加载需要大量的样板代码和一些补丁才能实现。
使用 Angular Ivy 会简单得多。考虑以下示例:点击图片,延迟加载 bundle,并将组件添加到视图。延迟加载可以提高应用程序的速度。 理想情况下,它会:
@Component(...)
export class AppComponent{
constructor(
private viewContainer: ViewContainer,
private cfr: ComponentFactoryResolver) {
// lazy click handler
async lazyload() {
// use the dynamic import
const {LazyComponent} = await import('./lazy/lazy.component');
this.viewContainer.createComponent(LazyComponent);
}
}
}
视图引擎必须通过ComponentFactoryResolver
将惰性组件解析为工厂并加载它来传递:
this.viewContainer.createComponent(this.cfr.resolveComponentFactory(LazyComponent));
捆绑包大小
为了评估打包文件大小的改进,Angular 团队使用了Hello World应用程序作为衡量指标。使用 Angular Ivy 构建后,最终最小化的打包文件大小约为 4.5kB,使用 Closure Compiler 构建后约为 2.7kB。
然后可以更有效地捆绑Angular Elements,而且 Ivy 已为未来的捆绑器/优化器做好了准备。
调试
全局对象中添加了一个新的 API ng
。在 ChromeDevTools 中,只需打开控制台并输入以下命令ng
即可查看新选项:
考虑从 Angular Material 库中获取一个<mat-drover></mat-drover>
组件,可以直接从控制台对该组件进行操作(感谢 Juri Strumpflohner 在他的教程中提供的示例):
// grab the component instance of the DOM element stored in $0
let matDrawer = ng.getComponent($0);
// interact with the component's API
matDrawer.toggle();
// trigger change detection on the component
ng.markDirty(matDrawer);
从“元素”选项卡中,只需选择调试操作的元素,$0
就会在其附近出现一个,它可以用作控制台中元素的选择器/占位符。
NgProbe
可能不再受支持:
在 Ivy 中,我们不支持 NgProbe,因为我们有自己的一套功能更强大的测试实用程序。
我们不应该引入 NgProbe,因为它会阻止 DebugNode 和朋友正确地进行树状摇动。
平台浏览器- Angular 源代码
结论
Angular 团队做得非常出色,很高兴参加 Angular Connect 2019 并看到去年推出的新渲染架构的改进。
现在可以在默认启用编译的情况下进行开发,aot
以避免开发和生产环境之间可能出现的不匹配。
另一个有趣的点是 Angular Elements。我认为得益于新的编译器和渲染引擎,这个项目现在确实可以加速了。目前,无法创建一个库项目并将其编译为 Web 组件,这真的是一个致命的功能。此外,生成的 Web 组件“内部包含太多 Angular 元素”,它们有点太大了,Ivy 应该减少包装 Angular 组件的框架体积。
真正令人印象深刻的是可以以非常简单的方式实现延迟加载,功能强大,但保持代码的可读性简单。
特别感谢
特别感谢
感谢同行评审,也感谢在启用 Ivy 的情况下发现 Angular 8 和 Angular 9 之间存在一些不准确之处。
参考
- Angular Connect 2019 主题演讲
- 深入探究 Angular 编译器
- 理解 Angular Ivy
- 选择 Angular Ivy
- 深入探究 Angular 编译器
- Angular 中的 Ivy 引擎
- 从 Devtools 控制台调试 Angular Ivy 应用程序