Angular Ivy - 第三代Angular渲染器的完整指南。
更小的捆绑包,更快的编译,更方便的调试,还有模块和组件的动态加载以及高阶组件等等高级概念。
一年多以前,Angular核心团队在ng-conf上宣布他们正在研究Angular Ivy,尽管它还没有100%准备好投入生产,但我觉得这是一个深入了解 Angular的新版本渲染器的好时机。
经过漫长的等待,Angular版本8发布!
这是一个主要版本,带来了许多很酷很重要的功能,例如差异加载,新构建器API,Web-Workers支持等等。
但最重要的是,Ivy终于来了!
正文
为什么关注Ivy
首先-移动设备!
这也许听起来疯狂,但确实我们有63%的在线流量来自智能手机和平板电脑。到今年年底,80%的互联网使用预计将来自移动设备。
我们面临的最大挑战之一是前端开发人员是加载网站尽可能的快。不幸的是移动设备经常因为坏或缓慢的互联网连接,使这种挑战变得更加困难。
另一方面,我们可以使用许多解决方案加载应用程序更快.例如从最近的云的CDN节点请求文件,PWA缓存的资产文件等。但是我们能为开发者做的是最大程度减少包的大小.
减少捆绑包大小
所以...捆绑大小。让我们看看它的实际效果。我们以eliassy.dev 作为案例研究。这是一个使用Angular构建的简单网站,它看起来很简单,但它使用了许多核心功能。它还使用Angular PWA包来支持离线和Angular Material与Animation模块。
在Ivy之前,我的主要重量超过500 kb。
现在让我们选择加入Ivy,编辑tsconfig.app.json
并添加一部分angularComplierOption
并设置enableIvy
为true
。对于新的Angular CLI项目,您可以--enableIvy
在运行ng new
脚本时使用该标志。
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
],
"angularCompilerOptions": {
"enableIvy": true
}
}
现在让我们再次使用构建应用程序 ng build —prod:
我们可以看到我们的捆绑包收缩了77KB,这是捆绑包大小的15%,这意味着我们网站的加载时间将快15%。
你们中的一些人可能会因为我们只削减了15%的捆绑大小而感到失望。原因是即使这是一个小项目,它仍然依赖于许多核心功能,而目前,Ivy主要是削减生成的代码,而不是框架本身。
Stephen Fluin刚刚发布核心团队仍在努力使捆绑包的尺寸更小:
“我们现在正在努力减少框架大小,以便在将Ivy作为默认设置之前,我们几乎在每种情况下都减少了实际应用程序的包大小。由于我们提供了新的引导方式,因此我们还可以获得额外的好处。
他是如何工作的
那么,它的背后是什么?它是如何工作的?
要理解我们需要深入了解编译器的内部。让我们创建这个简单的代码:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<div>
<span>{{title}}</span>
<app-child *ngIf="show"></app-child>
</div>
`,
styles: []
})
export class AppComponent {
title = 'ivy-tree-shaking';
show: boolean;
}
现在,让我们运行ngc
命令来生成转换后的代码:
- 对于视图引擎渲染器:
node_modules/.bin/ngc
/**
* @fileoverview This file was generated by the Angular template compiler. Do not edit.
*
* @suppress {suspiciousCode,uselessCode,missingProperties,missingOverride,checkTypes}
* tslint:disable
*/
import * as i0 from "@angular/core";
import * as i1 from "./child.component.ngfactory";
import * as i2 from "./child.component";
import * as i3 from "@angular/common";
import * as i4 from "./app.component";
var styles_AppComponent = [];
var RenderType_AppComponent = i0.ɵcrt({
encapsulation: 2,
styles: styles_AppComponent,
data: {}
});
export {
RenderType_AppComponent as RenderType_AppComponent
};
function View_AppComponent_1(_l) {
return i0.ɵvid(0, [(_l()(), i0.ɵeld(0, 0, null, null, 1, "app-child", [], null, null, null, i1.View_ChildComponent_0,
i1.RenderType_ChildComponent)), i0.ɵdid(1, 114688, null, 0, i2.ChildComponent, [], null, null)],
function (_ck, _v) {
_ck(_v, 1, 0);
}, null);
}
export function View_AppComponent_0(_l) {
return i0.ɵvid(0, [(_l()(),
i0.ɵeld(0, 0, null, null, 4, "div", [], null, null, null, null, null)), (_l()(),
i0.ɵeld(1, 0, null, null, 1, "span", [], null, null, null, null, null)), (_l()(),
i0.ɵted(2, null, ["", ""])), (_l()(),
i0.ɵand(16777216, null, null, 1, null, View_AppComponent_1)),
i0.ɵdid(4, 16384, null, 0, i3.NgIf, [i0.ViewContainerRef, i0.TemplateRef], {
ngIf: [0, "ngIf"]
}, null)],
function (_ck, _v) {
var _co = _v.component;
var currVal_1 = _co.show;
_ck(_v, 4, 0, currVal_1);
},
function (_ck, _v) {
var _co = _v.component;
var currVal_0 = _co.title;
_ck(_v, 2, 0, currVal_0);
});
}
export function View_AppComponent_Host_0(_l) {
return i0.ɵvid(0, [(_l()(), i0.ɵeld(0, 0, null, null, 1, "app-root", [], null, null, null, View_AppComponent_0,
RenderType_AppComponent)), i0.ɵdid(1, 49152, null, 0, i4.AppComponent, [], null, null)], null, null);
}
var AppComponentNgFactory = i0.ɵccf("app-root", i4.AppComponent, View_AppComponent_Host_0, {}, {}, []);
export {
AppComponentNgFactory as AppComponentNgFactory
};
//# sourceMappingURL=app.component.ngfactory.js.map
- For Ivy:
node_modules/.bin/ngc -p tsconfig.app.json
import {
Component
} from '@angular/core';
import * as i0 from "@angular/core";
import * as i1 from "@angular/common";
import * as i2 from "./child.component";
const _c0 = [4, "ngIf"];
function AppComponent_app_child_3_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵelement(0, "app-child");
}
}
export class AppComponent {
constructor() {
this.title = 'ivy-tree-shaking';
}
}
AppComponent.ngComponentDef = i0.ɵɵdefineComponent({
type: AppComponent,
selectors: [["app-root"]],
factory: function AppComponent_Factory(t) {
return new(t || AppComponent)();
},
consts: 4,
vars: 2,
template: function AppComponent_Template(rf, ctx) {
if (rf & 1) {
i0.ɵɵelementStart(0, "div");
i0.ɵɵelementStart(1, "span");
i0.ɵɵtext(2);
i0.ɵɵelementEnd();
i0.ɵɵtemplate(3, AppComponent_app_child_3_Template, 1, 0, "app-child", _c0);
i0.ɵɵelementEnd();
}
if (rf & 2) {
i0.ɵɵselect(2);
i0.ɵɵtextBinding(2, i0.ɵɵinterpolation1("", ctx.title, ""));
i0.ɵɵselect(3);
i0.ɵɵproperty("ngIf", ctx.show);
}
},
directives: [i1.NgIf, i2.ChildComponent],
encapsulation: 2
});
/*@__PURE__*/
i0.ɵsetClassMetadata(AppComponent, [{
type: Component,
args: [{
selector: 'app-root',
template: `
<div>
<span>{{title}}</span>
<app-child *ngIf="show"></app-child>
</div>
`,
styles: []
}]
}], null, null);
//# sourceMappingURL=app.component.js.map
它发生了很大的变化,但是一些主要的差异在这里很重要:
-
我们不再有factory文件,现在所有装饰器都转换为静态函数。在我们的例子中,
@Component
转换ngComponentDef
。 - 指令集发生了变化,因此tree shaking ,将小得多。
不仅仅是更小的捆绑包
如果我们看一下ngIf
转换代码的部分:
i0.ɵdid(4, 16384, null, 0, i3.NgIf, [i0.ViewContainerRef, i0.TemplateRef], { ngIf: [0, "ngIf"] }, null)],
出于某种原因,我的应用程序组件与ViewContainerRef
和TemplateRef
相关联,如果你想知道它们两个的来源,他们实际上是依赖NgIf指令实现的。
在Ivy中将变得更加简单,现在每个组件都引用了子组件或指令,但公共的API却更加清晰。意思就是当我们改变某些东西时,比如:NgIf
的实现,我们不需要重新编译所有东西,我们可以重新编译NgIf
而不是AppComponent
类。
通过这种方式,我们不仅实现了更小的捆绑,而且实现了更快的编译,以及将库推送到NPM的更简单的方法。
使用Ivy进行调试
Ivy还提供了更简单的调试API。
让我们用(input)
事件创建一个输入,并将它绑定到一个名为的不存在的函数search:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<input (input)="search($event)">
`,
styles: []
})
export class AppComponent {
}
在Ivy之前,当我尝试在输入中输入内容时,我们会在控制台中看到它:
使用Ivy,我们的控制台将更加丰富地了解我们从哪里获得错误:
所以我们通过Ivy获得了另一个目的,更好的模板调试!
动态加载
我们有一个简单的应用程序,有2个模块,应用程序模块和功能模块。功能模块将与路由器一起延迟加载,并将显示功能组件。所以,当我点击click me按钮时,我在网络中获得了功能模块:
Angular 8带来了一个用于加载模块的新API,它现在支持ES6动态导入。
之前:
const routes: Routes = [
{
path: 'feature',
loadChildren: './feature/feature.module#FeatureModule'
}
];
之后:
const routes: Routes = [
{
path: 'feature',
loadChildren: () => import('./feature/feature.module')
.then(({ FeatureModule }) => FeatureModule)
}
];
有了这个,我们为什么不直接在组件上尝试相同的导入?
结果如下:
它实际上是工作了!! 但是等等......发生了一件奇怪的事。我们已经加载了一个组件,但没有在模块中声明它。
那么,我们还应该在模块中声明组件吗?或者,模块现在可选吗?我们很快就会回答这个问题,但首先,让我们尝试将此组件添加到视图中。
为此,我们使用ɵrenderComponent
函数:
export class AppComponent {
loadFeature() {
import('./feature/feature/feature.component')
.then(({ FeatureComponent }) => {
ɵrenderComponent(FeatureComponent);
});
}
}
我在这里得到一个异常,这是对的,因为我们试图将组件附加到视图,但没有告诉谁是host元素对吗?
这里我们有两个选项,第一个 - 将
FeatureComponent
选择器添加到DOM,Angular将知道使用渲染选择器占位符的组件:
<button (click)="loadFeature()">Click Me</button>
<app-feature></app-feature>
<router-outlet></router-outlet>
或者renderComponent
有另一个签名获取配置,我们可以设置host。我们甚至可以添加一个不存在的host,Ivy会将其附加到它:
loadFeature() {
import('./feature/feature/feature.component')
.then(({ FeatureComponent }) => {
ɵrenderComponent(FeatureComponent, { host: 'my-container' });
});
}
模块是否仍然必要?
正如我们刚才所见,我们不需要在模块上声明一个组件。现在我们所有人都想知道我们是否真的需要模块?
为了回答这个问题,让我们创建另一个用例 - 现在FeatureComponent
将注入一个将在AppModule中声明和提供的配置:
export const APP_NAME: InjectionToken<string> =
new InjectionToken<string>('App Name');
@NgModule({
...,
providers: [
{provide: APP_NAME, useValue: 'Ivy'}
],
bootstrap: [AppComponent]
})
export class AppModule { }
FeatureComponent:
import { Component, OnInit, Inject } from '@angular/core';
import { APP_NAME } from 'src/app/app.module';
@Component({
selector: 'app-feature',
template: `
<p>
Hello from {{appName}}!
</p>
`,
styleUrls: ['./feature.component.scss']
})
export class FeatureComponent implements OnInit {
constructor(@Inject(APP_NAME) public appName: string) { }
ngOnInit() {
}
}
现在 - 如果我们再次尝试加载组件,我们会得到一个异常,因为我们的组件没有注入器:
在模块上没有声明组件也存在问题,我们实际上没有使用注射器。尽管如此,renderComponent
配置还允许我们声明一个Injector:
export class AppComponent {
constructor(private injector: Injector) {}
loadFeature() {
import('./feature/feature/feature.component')
.then(({ FeatureComponent }) => {
ɵrenderComponent(FeatureComponent, { host: 'my-container', injector: this.injector });
});
}
}
结果如下:
好极了!有用!
高阶元件(HOC)
正如我们刚刚看到的那样 - Angular现在更加动态,它还允许我们实现像HOC这样的高级概念。
什么是HOC?
HOC是一个函数,它获取一个组件并返回一个组件,但也影响它们之间的组件。
让我们通过HOC,将它作为装饰器添加到我们创建的基本组件 AppComponent
:
import { Component, ɵrenderComponent, Injector } from '@angular/core';
@HOC()
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
constructor(private injector: Injector) { }
loadFeature() {
import('./feature/feature/feature.component')
.then(({ FeatureComponent }) => {
ɵrenderComponent(FeatureComponent, { host: 'my-container', injector: this.injector });
});
}
}
export function HOC() {
return (cmpType) => {
const originalFactory = cmpType.ngComponentDef.factory;
cmpType.ngComponentDef.factory = (...args) => {
const cmp = originalFactory(...args);
console.log(cmp);
return cmp;
};
};
}
现在让我们利用HOC和动态导入的概念来创建一个惰性组件:
import { Component, ɵrenderComponent, Injector, ɵɵdirectiveInject, INJECTOR } from '@angular/core';
@LazyComponent({
path: './feature/feature/feature.component',
component: 'FeatureComponent',
host: 'my-container'
})
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
constructor(private injector: Injector) { }
loadFeature() {
import('./feature/feature/feature.component')
.then(({ FeatureComponent }) => {
ɵrenderComponent(FeatureComponent, { host: 'my-container', injector: this.injector });
});
}
afterViewLoad() {
console.log('Lazy HOC loaded!');
}
}
export function LazyComponent(config: { path: string, component: string, host: string }) {
return (cmpType) => {
const originalFactory = cmpType.ngComponentDef.factory;
cmpType.ngComponentDef.factory = (...args) => {
const cmp = originalFactory(...args);
const injector = ɵɵdirectiveInject(INJECTOR);
import(`${config.path}`).then(m =>
ɵrenderComponent(m[config.component], { host: config.host, injector }));
if (cmp.afterViewLoad) {
cmp.afterViewLoad();
}
return cmp;
};
return cmpType;
};
}
谈论几个有趣的点:
-
如何在没有Angular DI的情况下安装injector?还记得
ngc
命令吗?我用它来检查Angular如何在转换后的文件中翻译构造函数注入并找到directiveInject
函数:const injector = ɵɵdirectiveInject(INJECTOR);
-
我已经使用HOC函数创建了一个新的“生命周期”函数
afterViewLoad
,如果它存在于原始组件上,它将在延迟到组件被渲染后调用结果(直接加载):
摘要
我们刚刚学到的内容的快速摘要:
- Ivy,第三代Angular编译器就在这里!它具有向后兼容性,通过使用它,我们可以获得更小的捆绑包,更容易调试API,更快的编译和动态加载模块和组件。
- Angular与Ivy的未来看起来很令人兴奋,有像HOC这样的酷炫和令人兴奋的功能。
- Ivy还为Angular Elements设置了基础,使其在我们的Angular应用程序中变得更加流行。
- 试试看!这就像设置
enableIvy
标志一样简单true
!
谢谢阅读!
不足之处,欢迎访问DLLCN的学习笔记进行批评与讨论,一起成长,一起学习.