Angular-Ivy

所有你需要知道的关于Ivy,新的Angular引擎

由于官方文档并没有做Ivy的太多说明,出于好奇就有了这篇译文:)

原文链接: All you need to know about Ivy, The new Angular engine!


减少包的大小

现在让我们通过编辑tsconfig.app来选择加入Ivy。添加一节angularComplierOption设置和enableivytrue。对于新的Angular CLI项目,你可以在运行ng new脚本时使用 --enableIvy 标志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
],
"angularCompilerOptions": {
"enableIvy": true
}
}

现在让我们再次使用ng build -prod构建应用程序:

我们可以看到我们的包缩小了77KB,是包大小的15%,这意味着我们网站的加载时间提高了15%


工作原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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命令来生成转换后的代码与Ivy生成的代码有很多变化:

  1. 我们不再有工厂factory文件,现在所有的装饰器都转换成了静态函数。在我们的示例中,@Component转换为一个ngComponentDeffor示例。

2.指令集已经改变,所以它可以被摇树优化了,并边小了


不仅仅是把包变小了

如果我们看看ngIf指令编译后的代码:

1
i0.ɵdid(4, 16384, null, 0, i3.NgIf, [i0.ViewContainerRef, i0.TemplateRef], { ngIf: [0, "ngIf"] }, null)],

由于某些原因,我的app组件与ViewContainerRefTemplateRef相关联。如果你想知道那两个来自哪里,它们实际上是NgIf指令实现的依赖项。

Ivy中,这变得更加简单,每个组件现在都引用子组件或指令,尽量使公共API更加独立。它的意思是当我们改变某物时,NgIf的实现,我们不需要重新编译所有的东西,我们只需要重新编译·,而不是整个AppComponent类。

通过这种方式,我们不仅实现了更小的包,还实现了更快的编译,以及更容易地将库发送到NPM。


Debugging with Ivy

Ivy还提供了更简单的调试API。
让我们创建一个Input事件,并绑定到一个不存在的函数名为搜索:

1
2
3
4
5
6
7
8
9
10
11
12
import { Component } from '@angular/core';

@Component({
selector: 'app-root',
template: `
<input (input)="search($event)">
`,
styles: []
})
export class AppComponent {

}

在用Ivy之前,当我们尝试在输入框中输入一些内容时,我们会在控制台中看到:
ERROR TypeError: _co.search is not a function

有了Ivy,我们的控制台看起来会更有信息,告诉我们错误来自哪里:
ERROR TypeError: ctx.search is not a function


动态加载

Angular 8为加载模块带来了一个新的API,它现在支持ES6动态导入。

1
2
3
4
5
6
7
const routes: Routes = [
{
path: 'feature',
loadChildren: () => import('./feature/feature.module')
.then(({ FeatureModule }) => FeatureModule)
}
];

提出问题:既然如此,为什么不直接在组件上尝试相同的导入呢?

1
2
3
loadFeature() {
import('./feature/feature/feature.component')
.then(({FeatureComponent}) => FeatureComponent)

在Button上添加click事件 loadFeature

居然可以工作! !等等,奇怪的事情发生了。我们加载了一个组件,但没有在模块中声明它。
那么,我们还应该在模块中声明组件吗?或者,模块现在是可选的吗?我们将很快回答这个问题,但是首先,让我们尝试将这个组件添加到视图中。

为此我们使用ɵrenderComponent功能,对app.component做下修改:

1
2
3
4
5
6
7
8
export class AppComponent {
loadFeature() {
import('./feature/feature/feature.component')
.then(({ FeatureComponent }) => {
ɵrenderComponent(FeatureComponent);
});
}
}

我在这里得到一个异常,这是有意义的,因为我们试图将组件附加到视图,但没有告诉谁是主机元素,对吗?

这里我们有两个选择,第一个是将FeatureComponent选择器添加到DOM中,Angular会知道如何在选择器占位符上渲染组件:

1
2
3
<button (click)="loadFeature()">Click Me</button>
<app-feature></app-feature>
<router-outlet></router-outlet>

或者renderComponent有另一个签名,得到一个配置,我们可以设置主机。我们甚至可以添加一个不存在的主机,Ivy会将它附加到它:

1
2
3
4
5
6
loadFeature() {
import('./feature/feature/feature.component')
.then(({ FeatureComponent }) => {
ɵrenderComponent(FeatureComponent, { host: 'my-container' });
});
}

模块仍然是必需的吗

正如我们刚刚看到的,我们不需要在模块上声明组件。这让我们所有人怀疑我们是否真的需要模块?
为了回答这个问题,让我们创建另一个用例-现在FeatureComponent将注入一个配置,将在AppModule中声明和提供:

1
2
3
4
5
6
7
8
9
10
11
12
// app.module.ts
export const APP_NAME: InjectionToken<string> =
new InjectionToken<string>('App Name');

@NgModule({
...,
providers: [
{provide: APP_NAME, useValue: 'Ivy'}
],
bootstrap: [AppComponent]
})
export class AppModule { }

FeatureComponent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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配置也让我们声明一个注入器:

app.component.ts

1
2
3
4
5
6
7
8
9
export class AppComponent {
constructor(private injector: Injector) {}
loadFeature() {
import('./feature/feature/feature.component')
.then(({ FeatureComponent }) => {
ɵrenderComponent(FeatureComponent, { host: 'my-container', injector: this.injector });
});
}
}

结果是可以运行的!🎉🎉🎉


Higher Order Components (HOC)

正如我们刚刚看到的,Angular现在更加动态了,它还允许我们实现像HOC这样的高级概念。

什么是HOC?

HOC是一个函数,它获取一个组件并返回一个组件,但同时也影响其中的组件。

让我们创建基本的HOC通过添加它作为装饰到我们的AppComponent:

app.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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和动态导入的概念来创建一个惰性组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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;
};
}

1.以下是一些有趣的话题:
如何在没有Angular DI的情况下获得注入器?还记得ngc命令吗?我用它来检查Angular是如何在转换后的文件中转换构造函数注入的,并找到了directiveInject函数:

1
2
// const injector = ɵɵdirectiveInject(INJECTOR);
const injector = new directiveInject(INJECTOR);

2.我使用HOC函数创建了一个名为afterViewLoad的新“生命周期”函数,如果它存在于原始组件上,它将在延迟组件呈现后被调用

接下来就能正常运行了。

总结
快速总结一下我们刚刚学到的:

  • Ivy,第三代Angular编译器真的来了!它具有向后兼容性,通过使用它,我们可以得到更小的包,更容易调试API,更快的编译和模块和组件的动态加载。

  • 带有Ivy的Angular的未来看起来很激动人心,因为它拥有一些很酷很激动人心的特性,比如HOC。

  • Ivy还为Angular元素在我们的Angular应用程序中变得更加流行奠定了基础。
    试试吧!这就像设置enableIvy标志为真一样简单!