Sytuacja kobiet w IT w 2024 roku
1.07.201910 min
Eliran Eliassy

Eliran Eliassy Front-End Developer / Trainer500Tech

Przewodnik po Ivy - nowym silniku Angulara

Wszystko, co musisz wiedzieć o Ivy, kompilatorze Angulara trzeciej generacji.

Przewodnik po Ivy - nowym silniku Angulara

Mniejsze pakiety, szybsze kompilacje, lepsze debugowanie, dynamiczne ładowanie modułów i komponentów oraz zaawansowane koncepcje, takie jak komponenty wyższego rzędu. Angular Ivy  -  oto kompletny przewodnik po rendererze Angulara trzeciej generacji.

Oryginał tekstu w języku angielskim przeczytasz tutaj.

Ponad rok temu zespół Angular Core Team ogłosił na ng-conf, że pracują nad Angular Ivy, i chociaż nie jest on w 100% produkcyjnie gotowy, wydaje mi się, że teraz jest naprawdę dobry moment na bliższe zapoznanie się z nowym rendererem Angulara.

Po długim oczekiwaniu, ósma wersja Angulara jest dostępna tutaj! Jest to większe wydanie, które oferuje wiele fajnych (i ważnych) funkcji, takich jak Differential Loading, nowe API buildera, obsługa Web Workers i wiele więcej. Jednak przede wszystkim, Ivy wchodzi w końcu do gry!

Dlaczego Ivy?

Przede wszystkim  -  ze względu na urządzenia mobilne! Może to zabrzmi dziwnie, ale 63% całego ruchu internetowego w USA pochodzi ze smartfonów i tabletów. Do końca tego roku, 80% ruchu internetowego ma pochodzić z urządzeń mobilnych. (Źródło)

Jednym z największych wyzwań dla programistów jest zapewnienie jak najszybszego ładowania naszych stron internetowych. Urządzenia mobilne niestety często cierpią przez słabe lub powolne połączenie internetowe, co jeszcze bardziej utrudnia to wyzwanie.

Z drugiej strony, możemy użyć wielu różnych rozwiązań, aby załadować nasze aplikacje znacznie szybciej, np.: użyć CDN do obsługi plików z najbliższej chmury, PWA do przechowywania zasobów i wielu innych. Ale największą okazją, jaką mamy jako programiści, jest zmniejszenie rozmiaru pakietu.

Zmniejszenie rozmiaru pakietu

Więc....rozmiar pakietu. Zobaczmy, jak to wygląda w akcji. Dla przykładu użyjmy eliassy.dev. Jest to prosta strona internetowa zbudowana z Angularem i również wygląda na prostą, ale wykorzystuje wiele funkcji. Między innymi pakiet Angular PWA do obsługi strony w trybie offline i Angular Material z modułem do animacji.


eliassy.dev pracuje w trybie offline.

Przed Ivy, rozmiar całej paczki wynosił nieco ponad 500 kB.


Eliassy.dev główny build przed Ivy.

Teraz użyjmy Ivy poprzez edycję tsconfig.app.json i dodanie sekcji angularComplierOption gdzie ustawimy enableIvy na true. W nowych projektach Angular CLI, możesz po prostu użyć flagi --enableIvy podczas uruchamiania skryptu ng new.

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/app",
    "types": []
  },
  "exclude": [
    "test.ts",
    "**/*.spec.ts"
  ],
  "angularCompilerOptions": {
    "enableIvy": true
  }
}


Teraz zbudujmy aplikację ponownie używając ng build --prod:


Eliassy.dev główny build po Ivy.

Widzimy, że nasz pakiet skurczył się o 77KB, co stanowi 15% rozmiaru pakietu, co oznacza, że czas ładowania naszej strony internetowej będzie o 15% krótszy.

Niektórzy z Was mogą być rozczarowani faktem, że tniemy tylko 15% pakietu. Powodem tego jest to, że nawet jeśli jest to mały projekt, nadal opiera się on na wielu podstawowych funkcjach i na razie Ivy optymalizuje głównie wygenerowany kod, a nie sam framework.

Stephen Fluin właśnie ogłosił, że główny zespół nadal pracuje nad tym, aby rozmiar pakietu był jeszcze mniejszy:

"Obecnie pracujemy nad zmniejszeniem rozmiaru frameworku, tak aby zmniejszyć rozmiary pakietów dla prawdziwych aplikacji w prawie każdym przypadku. Chcemy to zrobić, zanim Ivy stanie się domyślnym silnikiem Angulara. Pojawią się też inne korzyści, ponieważ oferujemy nowe sposoby boostrappingu, które wycięłyby jeszcze więcej Angulara z głównego pakietu".

Jak to działa?

Więc, co za tym stoi? Jak to działa?

Aby to zrozumieć, musimy bliżej zapoznać się z kompilatorem. Stwórzmy taki oto prosty kod:

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


Teraz uruchomimy polecenie ngc, aby wygenerować transpilowany kod:

1. W rendererze view-engine: 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. W 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


Wiele się zmieniło, ale warto zauważyć kilka konkretnych różnic:

  1. Nie mamy już plików z fabrykami, teraz wszystkie dekoratory zostały przekształcone w funkcje statyczne. Dla przykładu, w naszym kodzie @Component przekształca się w ngComponentDef.
  2. Zestaw instrukcji zmienił się tak, że można użyć tree-shakingu, dzięki czemu będzie mniejszy.

Nie tylko mniejsze pakiety

Jeśli spojrzymy na sekcję ngIf transpilowanego kodu:

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


Z jakiegoś powodu mój komponent aplikacji jest związany z ViewContainerRef i TemplateRef. Jeśli zastanawiasz się, skąd się wzięły te dwa kawałki kodu, są one faktycznie zależne od implementacji dyrektywy NgIf.

W Ivy staje się to znacznie prostsze, każdy komponent odnosi się teraz do swoich dzieci lub dyrektyw, poprzez znacznie czystsze publiczne API. Oznacza to, że gdy coś zmienimy, np.: implementując NgIf, nie będziemy musieli przekompilować wszystkiego, możemy po prostu przekompilować NgIf, a nie klasę AppComponent.

W ten sposób uzyskaliśmy nie tylko mniejsze pakiety, ale także szybsze kompilacje a wysyłanie bibliotek do NPM będzie prostsze

Debugowanie z Ivy

Ivy zapewnia również znacznie łatwiejsze API debugowania.

Utwórzmy input ze zdarzeniem (input) i połączmy je z nieistniejącą funkcją o nazwie search:

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

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

}


Przed Ivy, kiedy próbowaliśmy wpisać coś wewnątrz inputu, dostawaliśmy taką odpowiedź w konsoli:

Z Ivy nasza konsola udzieli znacznie więcej informacji i powie skąd wziął się błąd:

Więc zdobyliśmy kolejny cel z Ivy, lepsze debugowanie szablonów!

Dynamiczne ładowanie

Mamy prostą aplikację, z 2 modułami, modułem aplikacji i feature module. Feature module ładuje się leniwie przez router i wyświetli feature component. Więc, kiedy kliknę przycisk "kliknij mnie", dostanę przez sieć kod tego modułu:

Angular 8 wprowadza nowe API dla ładowania modułów i teraz obsługuje dynamiczny import ES6.

Wcześniej:

const routes: Routes = [
  {
    path: 'feature',
    loadChildren: './feature/feature.module#FeatureModule'
  }
];


Teraz:

const routes: Routes = [
  {
    path: 'feature',
    loadChildren: () => import('./feature/feature.module')
      .then(({ FeatureModule }) => FeatureModule)
  }
];


Mimo to, dlaczego by nie spróbować zrobić dokładnie ten sam import bezpośrednio w komponencie?


Oto wynik:

To naprawdę działa! Ale czekaj.... Zauważyłeś, że stało się coś dziwnego? Załadowaliśmy komponent, bez deklarowania go w module.

Więc, czy nadal musimy deklarować komponenty w modułach? Czy też moduły są teraz opcjonalne? Odpowiemy na to wkrótce, ale najpierw spróbujmy dodać ten komponent do widoku.

W tym celu użyjmy funkcji `ɵrenderComponent`:

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

Zwraca mi to wyjątek, co ma sens, ponieważ staramy się dołączyć komponent do widoku, ale nie wskazaliśmy elementu, który będzie hostem, prawda?

Tutaj mamy dwie opcje, pierwszą jest dodanie selektora FeatureComponent do DOM, a Angular będzie wiedział, jak renderować komponent na zastępczym selektorze:

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


Lub, jeśli renderComponent ma inną sygnaturę, przekazanie konfiguracji, w której ustawimy hosta. Możemy nawet użyć nieistniejącego hosta, a Ivy go użyje:

Czy moduły są nadal potrzebne?

Jak właśnie widzieliśmy, nie musimy deklarować komponentu w module. Przez to można się zastanawiać, czy naprawdę potrzebujemy modułów?

Aby odpowiedzieć na to pytanie, utwórzmy kolejny przykład użycia, tym razem FeatureComponent wstrzyknie konfigurację, która zostanie zadeklarowana i podana w 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() {
  }

}


Jeśli teraz spróbujemy ponownie załadować komponent, otrzymamy wyjątek, ponieważ nasz komponent nie ma injectora:

Istnieją również minusy deklarowania komponentu poza modułem, gdyż tak naprawdę nie uzyskujemy razem z nimi injectorów. Pomimo tego, config renderComponent pozwala nam również zadeklarować injector:

export class AppComponent {
  constructor(private injector: Injector) {}
  loadFeature() {
    import('./feature/feature/feature.component')
      .then(({ FeatureComponent }) => {
        ɵrenderComponent(FeatureComponent, { host: 'my-container', injector: this.injector });
      });
  }
}


A oto wynik:

Yay! To działa!

Komponenty wyższego rzędu (Higher Order Components, HOC)

Jak właśnie widzieliśmy  -  Angular jest teraz znacznie bardziej dynamiczny, a także pozwala nam wdrażać zaawansowane koncepcje, takie jak HOC.

Co to jest HOC?

HOC jest funkcją, która pobiera komponent i zwraca komponent, a w międzyczasie może na nie wpływać.

Stwórzmy podstawowy HOC, dodając go jako dekorator do naszego 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;
    };
  };
}


Teraz wykorzystajmy koncepcję HOC i dynamicznego importu, aby stworzyć leniwy komponent:

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


Kilka ciekawych punktów które warto omówić:

1. Jak pozyskać injector bez DI Angulara? Pamiętasz polecenie NGA? Użyłem go, aby sprawdzić, jak Angular tłumaczy wstrzyknięcie konstruktora wewnątrz przetranspilowanych plików i znalazłem funkcję directiveInject:

const injector = ɵɵdirectiveInject(INJECTOR);

2. Użyłem funkcji HOC do stworzenia nowej funkcji "cyklu życia" o nazwie afterViewLoad, która jeśli znajduje się w oryginalnym komponencie, zostanie wywołana po wyrenderowaniu leniwego komponentu.

Wynik (bezpośrednio przy ładowaniu):

Podsumowanie

Szybkie podsumowanie tego, czego się właśnie nauczyliśmy:

  1. Ivy, trzecia generacja kompilatora Angulara jest w pełni dostępna! Posiada wsteczną kompatybilność a używając jej, możemy uzyskać mniejsze pakiety, prostsze API do debugowania, szybszą kompilację i dynamiczne ładowanie modułów i komponentów.
  2. Przyszłość Angulara z Ivy wygląda ekscytująco, dzięki fajnym i ciekawym funkcjom, takim jak HOC.
  3. Ivy stanowi również podstawę do tego, aby Angular Elements stały się znacznie bardziej popularne w naszych angularowych aplikacjach.
  4. Wypróbuj Ivy! Wystarczy jedynie ustawić flagę enableIvy na true!


Dzięki za Twój czas!

<p>Loading...</p>