Angular

Около года назад команда Angular объявила на ng-conf (конференция по Angular), что она работает над Angular Ivy. Хотя он еще не на 100% готов выйти в продакшн, я чувствую, что сейчас самое время углубиться в новый рендер для Angular .

После долгого ожидания, Angular 8 уже здесь!

Skillbox

Этот важный релиз принесет множество классных (и важных) фишек, как, например, дифференциальная загрузка, новый компилятор для API, поддержка Web-Workers и еще многое другое.

Почему Ivy?

Самое главное — мобильные устройства!

Возможно, это прозвучит невероятно, но 63% всего онлайн трафика США исходит от телефонов и планшетов. К концу этого года 80% пользователей интернета будут заходить именно с мобильных устройств. (Источник )

Одна из самых больших сложностей для фронтенд разработчиков — это максимально быстрая загрузка сайта. У мобильных устройств, к сожалению, часто плохое интернет соединение, из-за чего разработка только усложняется.

С другой стороны, есть множество способов загрузить приложение быстрее: использование CDN для получения файлов из облака, PWA для кэширования изображений и многое другое. Но самое продуктивное решение — это уменьшение размера бандла.

Уменьшаем размер бандла

Итак, размер бандла. Посмотрим на это в действии. Возьмем eliassy.devв качестве примера. Это простой веб-сайт, построенный на Angular. Выглядит просто, но у него есть множество интересных фич. Также он использует PWA пакеты для поддержки оффлайн режима и Angular Material с модулем для анимации.

До Ivy размер моего основного бандла составлял чуть больше 500KB.

Теперь подключим Ivy. Для этого в файле tsconfig.app.json нужно добавить флаг enableIvy к true в разделе angularComplierOption. Для проектов на 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 же урезает сгенерированный код, а не сам фреймворк.

Так как же все работает?

Для того, чтобы понять это, нам необходимо копнуть глубже в компилятор. Создадим простой код.

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

2. Для 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. У нас больше нет заводских файлов, теперь все декораторы конвертируются в статические функции. В нашем примере декоратор @Component превращается в ngComponentDef.

2. Изменился набор инструкций: появилась поддержка 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, а не весь AppComponent класс.

Таким образом мы добились не только меньшего размера бандла, но также ускоренной компиляции и упрощенной передачи библиотек в NPM.

Дебаггинг с Ivy

Ivy также предоставляет возможность для простого дебага API.

Давайте создадим input с помощью события (input)и привяжем его к несуществующей функции search:

import { Component } from '@angular/core';

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

}

До Ivy при попытке написать что-то в input в консоли появлялось следующее:

С Ivy консоль будет выглядеть информативнее и будет понятно, откуда взялась ошибка:

Так что с Ivy у нас появилась еще одна возможность — простой дебаг шаблонов.

Динамическая загрузка

У нас есть простое приложение с 2 модулями: AppModule иFeatureModule. Feature модуль будет загружен «лениво» с помощью маршрутизатора и отобразит компонент feature. Когда я кликну на кнопку “click me” , в сети у меня появится feature модуль.

https://stackblitz.com/edit/ivy-example

В 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)
  }
];

Почему бы с этим не попробовать прямой импорт в компонент?

https://stackblitz.com/edit/ivy-example-dynamic-component-load

И в результате:

Работает! Однако произошло что-то странное. Мы загрузили компонент, не объявляя его в модуле. Так нужно ли объявлять компоненты в модулях? Или модули теперь не обязательны? Мы узнаем об этом позже. Сначала загрузим этот компонент в представление.

Для этой цели мы будем использовать функцию renderComponent:

export class AppComponent {
  loadFeature() {
    import('./feature/feature/feature.component')
      .then(({ FeatureComponent }) => {
        ɵrenderComponent(FeatureComponent);
      });
  }
}

Появляется ошибка, так как мы пытаемся привязать компонент к его представлению, но при этом не сказали кто будет хостом этого элемента, так?

Здесь есть два варианта: первый  — добавить селектор FeatureComponent в DOM, чтобы Angular знал, что нужно подгрузить компонент в месте, где есть селектор.

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

Второй вариант — у renderComponent есть еще одна подписка на конфигурацию, где можно назначить хоста. Мы даже можем добавить несуществующий хост:

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 });
      });
  }
}

И в результате:

Ура! Работает!

Компоненты высшего порядка 

Как мы только что убедились — 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? Помните ngc команду? Я использовал ее для проверки того, как Angular делает инъекцию в конструкторе в передаваемые файлы, и обнаружил функцию directiveInject:
const injector = ɵɵdirectiveInject(INJECTOR);

2. Я использовал HOC функцию для создания новой функции из «цикла жизни компонента», которая называется afterViewLoad , так, будто она встроенная. В итоге она будет вызвана после того, как ленивый компонент загрузится.

Результат (сразу после загрузки):

Итог:

1. Ivy, рендер третьего поколения от Angular уже здесь! У него есть обратная совместимость, и мы можем его использовать для уменьшения размера бандла, упрощенного дебага API, ускоренной компиляции и динамической загрузки модулей и компонентов.

2. Будущее Angular с Ivy выглядит интригующим с такими классными вещами, как HOC.

3. Ivy также заложил основу для Angular элементов, которые теперь будут более популярны в наших Angular приложениях.

4. Попробуйте! Это так же просто, как поставить enableIvy флаг в настройках true.

Читайте также:


Перевод статьи Eliran Eliassy: All you need to know about Ivy, The new Angular engine!