Около года назад команда Angular объявила на ng-conf (конференция по Angular), что она работает над Angular Ivy. Хотя он еще не на 100% готов выйти в продакшн, я чувствую, что сейчас самое время углубиться в новый рендер для Angular .
После долгого ожидания, Angular 8 уже здесь!
Этот важный релиз принесет множество классных (и важных) фишек, как, например, дифференциальная загрузка, новый компилятор для 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
, чтобы сгенерировать код.
- Для обычного рендера:
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;
};
}
Несколько интересных моментов:
- Как получить инжектор без 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
.
Специально для сайта ITWORLD.UZ. Новость взята с сайта NOP::Nuances of programming