Ivy voll integriert, Lazy-Loading-Komponenten und Lokalisierung

Angular 9 ist da: Ivy, Lazy Loading und mehr

Manfred Steyer

© Shutterstock / Devina Saputri

Angular 9 bringt Ivy in einer abwärtskompatiblen Variante und somit kleinere Bundles. Außerdem wurde die I18N-Lösung umfangreich überarbeitet und einige Ecken wurden abgerundet. Damit stehen nicht nur dem Entwickler, sondern auch den zukünftigen Versionen von Angular neue Möglichkeiten zur Verfügung.

Mit Angular 9 soll es so weit sein: Ivy wird endlich Standard. Das ist möglich, weil das Angular-Team sehr viel Energie investiert hat, um Ivy abwärtskompatibel zum Vorgänger ViewEngine zu machen. Ein Blick auf den Change Log verrät, dass genau das der Fokus von Version 9 war. Abseits davon gibt es aber trotzdem ein paar sehr nette Neuerungen. In diesem Artikel stelle ich anhand von Beispielen diejenigen vor, die uns die tägliche Arbeit vereinfachen werden. Dazu verwende ich ein paar Beispiele, die sich in meinem GitHub Repository finden.

Update

Das Update auf Angular 9 ist, wie schon von den Vorgängerversionen gewohnt, sehr geradlinig. Der CLI-Befehl ng update @angular/core @angular/cli reicht aus, um die Bibliotheken auf den neuesten Stand zu heben. Eventuelle Breaking Changes werden dabei soweit als möglich automatisiert berücksichtigt. Möglich machen das Migrationsskripte, die auf dem CLI-internen Codegenerator Schematics basieren und den bestehenden Quellcode geringfügig modifizieren.
Für die Fälle, in denen wir dennoch manuell eingreifen müssen, geben die verwendeten Schematics eine Meldung aus. Außerdem findet man weitere Informationen zur Migration wie gewohnt unter Angular.io.

Ivy by default

Die wichtigste Neuerung bei Angular 9 ist wohl, dass der neue Ivy-Compiler, an dem seit vielen Monaten gearbeitet wird, standardmäßig aktiviert ist. Das bedeutet, dass unsere Bundles nach der Umstellung auf Version 9 um bis zu 40 Prozent kleiner werden. Wie sehr unsere Anwendung dieses Potenzial ausschöpfen kann, hängt von ihrem Aufbau ab.

Um Breaking Changes zu vermeiden, wurde ein besonderes Augenmerk auf die Abwärtskompatibilität gelegt. Das hat auch etwas damit zu tun, dass bei Google über 1 500 Angular-Anwendungen im Einsatz sind. Diese Anwendungen sollen selbstverständlich auch noch nach der Umstellung auf Version 9 funktionieren. Gleichzeitig halfen sie, die Qualität von Ivy sicherzustellen. Zusätzlich kam eine Vielzahl an weit verbreitenden Angular-Bibliotheken zur Qualitätssicherung zum Einsatz.

Da das Angular-Team jedoch den gesamten Unterbau austauschen musste, ist Ivy alles andere als ein einfaches Unterfangen. Deswegen kann es vorkommen, dass Angular Ivy in Randfällen den einen oder anderen Fehler zu Tage fördert. In diesen Fällen können wir Ivy mit der Eigenschaft enableIvy in der tsconfig.json deaktivieren:

"angularCompilerOptions": {
  "enableIvy": false
}

Das Angular-Team ist an Fällen interessiert, in denen das notwendig ist, und freut sich über entsprechende Bug Reports.

Lazy Loading von Komponenten

Auf dem ersten Blick bringt Ivy kleinere Bundles. Doch die Architektur von Ivy hat noch viel mehr zu bieten, sodass wir in den nächsten Releases neue, darauf aufbauende Features erwarten können.

Ein neues Feature ist jedoch heute schon verfügbar: Lazy Loading von Komponenten. Während Lazy Loading zwar schon von Anfang an integriert war, mussten bis dato immer ganze Angular-Module geladen werden. Der Grund war, dass ViewEngine die Metadaten für den Einsatz der einzelnen Komponenten auf Modulebene untergebracht hat. Ivy verstaut diese Metadaten nun jedoch beim Kompilieren direkt in den Komponenten, somit können diese auch separat bezogen werden.

Um den Einsatz dieser neuen Möglichkeit zu verdeutlichen, nutze ich hier ein einfaches Dashboard, das die anzuzeigenden Kacheln per Lazy Loading bezieht (Abb. 1).

Abb. 1: Dashboard mit Lazy Loading

Abb. 1: Dashboard mit Lazy Loading

Hierfür benötigen wir zunächst Platzhalter, in den die Komponente geladen werden kann. Das kann jeder beliebige Tag sein, solange er mit einer Templatevariable markiert wird:

<ng-container #vc></ng-container>

Templatevariablen beginnen, wie man hier sieht, mit einer Raute. Die zugehörige Komponente kann dieses Element als ViewChild laden (Listing 1).

export class DashboardPageComponent implements OnInit, OnChanges {

  @ViewChild('vc', {read: ViewContainerRef, static: true}) 
  viewContainer: ViewContainerRef;

  […]

  constructor(
    private injector: Injector,
    private cfr: ComponentFactoryResolver) { }

[…]
}

Außerdem benötigen wir für das dynamische Erzeugen der lazy-Komponente den aktuellen Injector sowie einen ComponentFactoryResolver. Beides lässt sich in den Konstruktor injizieren.

Danach ist alles sehr geradlinig: Über einen dynamischen Import lässt sich die lazy-Komponente laden und der ComponentFactoryResolver ermittelt die Factory, die wir zum Instanziieren der Komponente benötigen (Listing 2).

import('../dashboard-tile/dashboard-tile.component').then(m => {
  const comp = m.DashboardTileComponent;

  // Only b/c of compatibility; will not be needed in future!
  const factory =
    this.cfr.resolveComponentFactory(comp);

  const compRef = this.viewContainer.createComponent(
    factory, null, this.injector);

  const compInstance = compRef.instance;

  compInstance.a = Math.round(Math.random() * 100);
  compInstance.b = Math.round(Math.random() * 100);
  compInstance.c = Math.round(Math.random() * 100);
  compInstance.ngOnChanges();
});

Die Methode createComponent des ViewContainer nimmt die Factory entgegen und erzeugt eine Instanz der Komponente. Damit sie beim Dependency-Injection-Mechanismus von Angular angeschlossen wird, gilt es auch, den aktuellen Injector zu übergeben. Anschließend ruft das Beispiel die Komponenteninstanz ab, setzt Eigenschaften und ruft die ngOnChanges-Methode auf. Das Ergebnis ist eine DashboardTileComponent, die mitten im ViewContainer erscheint.

Eine weitere kleine, aber feine Neuerung ist, dass die Anwendung solche dynamischen Komponenten nicht mehr in den entryComponents registrieren muss. Dieses für viele ohnehin schwer verständliche Konstrukt existiert nur mehr für den Fall eines Fallbacks auf ViewEngine.

Besseres I18N mit @angular/localize

Die seit Version 2 in Angular integrierte Lokalisierungslösung (I18N) war in erster Linie auf Performance getrimmt. Sie erzeugt pro Locale (Kombination aus Sprache und Land) einen Build und passt beim Kompilieren die Übersetzungstexte ein, sodass sich zur Laufzeit kein Overhead ergibt. Diese Strategie hatte jedoch auch ein paar Nachteile:

  • Das Erstellen all dieser Builds war zeitintensiv
  • Es bestand keine Möglichkeit, die Übersetzungstexte zur Laufzeit festzulegen
  • Es war nicht möglich, Übersetzungstexte programmatisch zu nutzen
  • Die Sprache konnte nicht zur Laufzeit geändert werden. Stattdessen musste die Anwendung den Benutzer auf die gewünschte Sprachversion weiterleiten

Mit Angular 9 erscheint eine neue Lösung, die die meisten dieser Nachteile kompensiert und dabei trotzdem keine Abstriche in Sachen Performance macht. Sie nennt sich @angular/localize und lässt sich über das gleichnamige npm-Paket beziehen. Das neue @angular/localize erzeugt zunächst einen einzigen Build. Pro Locale legt sie danach eine Kopie an und bringt dort die Übersetzungstexte ein (Abb. 2).

Abb. 2: Funktionsweise von @angular/localize [1]

Abb. 2: Funktionsweise von @angular/localize [1]

Außerdem ergänzt @angular/localize jede Kopie um Metadaten für die Formatierung von Zahlen und Datumswerten entsprechend der Gepflogenheiten der jeweiligen Sprache. Wir müssen also nicht mehr die benötigten Metadaten beim Programmstart importieren und registrieren. Wie im Angular-Umfeld üblich lässt sich die Bibliothek via ng add beziehen:

ng add @angular/localize

Danach können wir die zu übersetzenden Texte in den Templates mit dem Attribut i18n versehen. Um dem Übersetzungsstudio Kontextinformationen zu bieten, erhält dieses Attribut einen Wert mit der Bedeutung und einer Beschreibung:

<h1 i18n="meaning|description@@home.hello">Hello World!</h1>

Beide Informationen sind optional und werden mit einer Pipe voneinander getrennt. Ebenso optional ist die ID, die nach zwei @-Symbolen erscheinen kann. Fehlt diese ID, erzeugt Angular selbst eine. Das ist jedoch problematisch, denn wenn sich das Markup ändert, vergibt Angular einen neuen Wert dafür.

Nachdem alle Texte mit i18n markiert wurden, extrahiert der Befehl ng xi18n sie in eine XML-Datei. Diese ist pro Sprache zu kopieren und um entsprechende Übersetzungen zu ergänzen (Listing 3).

<trans-unit id="home.hello" datatype="html">
  <source>Hello World!</source>
  <target>Hallo Welt!</target>
  <context-group purpose="location">
    <context context-type="sourcefile">src/app/app.component.html</context>
    <context context-type="linenumber">1</context>
  </context-group>
  <note priority="1" from="description">description</note>
  <note priority="1" from="meaning">meaning</note>
</trans-unit>

Standardmäßig liegen diese Dateien im XLF-(XML-Localization-Interchange-File-)Format vor. Alternativ dazu lässt sich das CLI anweisen, stattdessen XLF2 oder XMB (XML Message Bundles) zu nutzen. Diese Formate werden häufig von Übersetzungsstudios unterstützt.

Die übersetzten Dateien sind danach in der angular.json zu registrieren (Listing 4).

"i18n": {
  "locales": {
    "de": "messages.de.xlf",
    "fr": "messages.fr.xlf"
  },
  "sourceLocale": "en-US"
},

Mit ein paar weiteren Eigenschaften lässt sich auch das gewählte Format festlegen, sofern vom Standardformat XLF abgewichen wurde. Außerdem ist hier das Locale, in dem die Templates vor der Übersetzung vorliegen, als sourceLocale einzutragen.

Die Anweisung ng build –localize erzeugt nun eine Version pro Locale (Abb. 3). Wie erwähnt, lässt sich diese Aufgabe verhältnismäßig schnell erledigen, weil das CLI im Gegensatz zu früher nur ein einziges Mal kompiliert und dann nur noch die Texte tauscht. Startet man nun einen Webserver im Verzeichnis dist/projektname, lässt sich eine der Sprachversionen durch Anhängen des jeweiligen Locales an den URL wählen.

Abb. 3: Sprachversionen im dist-Ordner

Abb. 3: Sprachversionen im dist-Ordner

Das neue @angular/localize erlaubt jedoch auch die Nutzung der Übersetzungstexte zur Laufzeit. Dazu sind sogenannte Tagged Template Strings zu nutzen:

title = $localize`:@@home.hello:Hello World!`;

Als Tag kommt hier der globale Bezeichner $localize zum Einsatz. Außerdem wird der String mit der ID des jeweiligen Texts eingeleitet. Diese befindet sich zwischen zwei Doppelpunkten. Danach kommt der Standardwert. Derzeit extrahiert ng xi18n solche Texte zwar noch nicht automatisch, aber das hält uns nicht davon ab, bestehende Einträge auf diese Weise zu nutzen oder neue manuell in die XML-Dateien einzutragen.

Eine weitere sehnlich erwartete Möglichkeit ist das Festlegen der Übersetzungstexte zur Laufzeit. Um diese Runtime Translations zu unterstützen, sind sie lediglich in Form von Key/Value-Pairs festzulegen:

import { loadTranslations } from '@angular/localize';

loadTranslations({
  'home.hello': 'Küss die Hand!'
});

Allerdings muss das vor dem Laden von Angular erfolgen, weswegen diese Codestrecke in ein eigenes Bundle auszulagern ist. Aus diesem Grund weist das beiliegende Beispiel diesen Aufruf in der polyfills.ts auf. Außerdem muss sich hier die Anwendung selbst ums Laden der richtigen Metadaten für die Formatierung von Zahlen und Datumswerten kümmern.

Any und platform

Die mit Version 6 eingeführten Treeshakable Providers machen die Arbeit mit Services um einiges einfacher. Version 9 setzt da noch einen drauf, indem es zwei weitere Werte für providedIn festlegt: any und plattform:

@Injectable({ providedIn: 'any' })
export class LoggerConfig {
  loggerName = 'Default';
  enableDebug = true;
}

Die Einstellung any bewirkt, dass jeder Scope auf Modulebene eine eigene Service-Instanz erhält. Das bedeutet, dass es für alle lazy-Module eine eigene Instanz gibt sowie eine weitere für die Gesamtheit der restlichen Nicht-lazy-Module. Scopes auf Komponentenebene betrifft das nicht.

Hierdurch lässt sich die Notwendigkeit für forRoot und vor allem forChild-Methoden reduzieren. Letztere haben jedoch nach wie vor den Vorteil, dass sie auch Konfigurationsparameter entgegennehmen können. Beim Einsatz von any muss die Anwendung diese auf andere Weise festlegen, z. B. im Konstruktor des jeweiligen Modules (Listing 5).

@NgModule({ […] })
export class FlightBookingModule {
  constructor(private loggerConfig: LoggerConfig) {
    loggerConfig.loggerName = 'FlightBooking';
    loggerConfig.enableDebug = false;
  }
}

Die ebenfalls ergänzte Einstellung platform registriert einen Service im Platform-Injektor. Dieser befindet sich eine Ebene über root und beherbergt Angular-interne Services.

Dev-Server für Server-side Rendering

Wenngleich auch Server-side Rendering (SSR) sicher kein Thema ist, das jedes Projekt benötigt, ist es dennoch für das Angular-Team strategisch wichtig. Es erlaubt nämlich, mit Angular in den Bereich öffentlicher, SEO-kritischer Seiten vorzudringen. Bis jetzt war das Entwickeln solcher Lösungen jedoch lästig, zumal der Entwicklungswebserver nur die browserbasierte und nicht die serverbasierte Version des Projekts nach einer Änderung neu erstellte. Um auch die Auswirkung auf den serverseitigen Betrieb zu testen, musste man einen neuen Build erzeugen.

Damit ist nun Schluss, denn für SSR steht ein Builder zur Verfügung, der beide Versionen neu erstellt und daraufhin das Browserfenster aktualisiert. Wir sehen somit sofort alle Auswirkungen unserer Änderungen.

Um in den Genuss dieser Lösung zu kommen, ist nach dem Hinzufügen des entsprechenden @nguniversal-Pakets lediglich das npm-Skript dev:ssr zu starten:

ng add @nguniversal/express-engine@next
npm run dev:ssr

Da hier zwei Builds parallel stattfinden müssen, ist die Performance nicht ganz so gut wie beim klassischen Development-Server. Nichtsdestotrotz verbessert es das Entwicklererlebnis erheblich gegenüber dem Status quo.

Fazit und Ausblick

Mit Angular 9 bekommen wir das lang ersehnte Ivy. Dank seines durchdachten Aufbaus macht es Angular selbst besser treeshakable und führt somit in vielen Fällen zu deutlich kleineren Bundles. Außerdem ermöglicht es Lazy Loading von Komponenten.

Um Ivy zu unterstützen, musste auch die integrierte I18N-Lösung überarbeitet werden. Das hat das Angular-Team zum Anlass genommen, die Implementierung zu verbessern: Der Build-Vorgang wurde drastisch beschleunigt, und wir können Übersetzungstexte programmatisch bereitstellen, aber auch konsumieren. Daneben erleichtert die Einstellung any für providedIn das Konfigurieren von Bibliotheken, indem es dafür sorgt, dass jedes lazy-Modul eine eigene Instanz erhält. Es kann eine Alternative zu forRoot und + darstellen und ist Mosaiksteinchen bei Bestrebungen, das Angular-Modulsystem optional zu gestalten.

Auch wenn sich das alles sehr aufregend anhört, wird es erst nach Angular 9 so richtig spannend. Denn Ivy bietet Potenzial für viele Neuerungen, denen sich das Angular-Team nun widmen kann.

Geschrieben von
Manfred Steyer
Manfred Steyer
Manfred Steyer ist selbstständiger Trainer und Berater mit Fokus auf Angular 2. In seinem aktuellen Buch "AngularJS: Moderne Webanwendungen und Single Page Applications mit JavaScript" behandelt er die vielen Seiten des populären JavaScript-Frameworks aus der Feder von Google. Web: www.softwarearchitekt.at
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: