新的Angular引擎Ivy

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。

title

现在让我们选择加入Ivy,编辑tsconfig.app.json并添加一部分angularComplierOption并设置enableIvytrue。对于新的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:

title

我们可以看到我们的捆绑包收缩了77KB,这是捆绑包大小的15%,这意味着我们网站的加载时间将快15%。


title

你们中的一些人可能会因为我们只削减了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命令来生成转换后的代码:

  1. 对于视图引擎渲染器: 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
  1. 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

它发生了很大的变化,但是一些主要的差异在这里很重要:

  1. 我们不再有factory文件,现在所有装饰器都转换为静态函数。在我们的例子中,@Component转换ngComponentDef
  2. 指令集发生了变化,因此tree shaking ,将小得多。

不仅仅是更小的捆绑包

如果我们看一下ngIf转换代码的部分:

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

出于某种原因,我的应用程序组件与ViewContainerRefTemplateRef相关联,如果你想知道它们两个的来源,他们实际上是依赖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之前,当我尝试在输入中输入内容时,我们会在控制台中看到它:


title

使用Ivy,我们的控制台将更加丰富地了解我们从哪里获得错误:


title

所以我们通过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元素对吗?

title

这里我们有两个选项,第一个 - 将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() {
  }

}

现在 - 如果我们再次尝试加载组件,我们会得到一个异常,因为我们的组件没有注入器:

title

在模块上没有声明组件也存在问题,我们实际上没有使用注射器。尽管如此,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;
  };
}

谈论几个有趣的点:

  1. 如何在没有Angular DI的情况下安装injector?还记得ngc命令吗?我用它来检查Angular如何在转换后的文件中翻译构造函数注入并找到directiveInject函数:

    const injector = ɵɵdirectiveInject(INJECTOR);
    
  1. 我已经使用HOC函数创建了一个新的“生命周期”函数afterViewLoad,如果它存在于原始组件上,它将在延迟到组件被渲染后调用

    结果(直接加载):

title

摘要

我们刚刚学到的内容的快速摘要:

  1. Ivy,第三代Angular编译器就在这里!它具有向后兼容性,通过使用它,我们可以获得更小的捆绑包,更容易调试API,更快的编译和动态加载模块和组件。
  2. Angular与Ivy的未来看起来很令人兴奋,有像HOC这样的酷炫和令人兴奋的功能。
  3. Ivy还为Angular Elements设置了基础,使其在我们的Angular应用程序中变得更加流行。
  4. 试试看!这就像设置enableIvy标志一样简单true

谢谢阅读!

原文链接

不足之处,欢迎访问DLLCN的学习笔记进行批评与讨论,一起成长,一起学习.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351