Angular 8

Angular 8 ist da: Ivy-Preview, Service Worker und Differential Loading – das sind die neuen Features
1 Kommentar

Angular 8 bringt eine Vorab-Version von Ivy, Unterstützung für Service Worker, Differential Loading und weitere kleine Abrundungen mit. Jetzt ist die neue Major-Version verfügbar. Was ändert sich für Entwickler? Hier sind die neuen Features im Überblick.

Ende Mai 2019 ist Angular 8 erschienen. Wie geplant gab es keine Überraschungen: Das Update des Frameworks und CLI erfolgt geradlinig mit ng update. Die neuen Features sind eine willkommene Ergänzung, ganz nach dem Motto: „Evolution statt Revolution“.

In diesem Artikel gehe ich auf die wichtigsten Neuerungen von Angular 8 und des dazugehörigen Angular CLI 8 ein. Die verwendeten Beispiele stehen auf GitHub zur Verfügung.

Vorgeschmack auf Ivy

Die nächste große Neuerung, auf die die Angular-Welt wartet, ist Ivy. Dabei handelt es sich um einen neuen Angular-Compiler sowie um eine neue Rendering-Pipeline. Ivy hat das Potential, deutlich kleiner Bundles zu generieren, macht inkrementelles Kompilieren einfacher und ist die Grundlage für künftige Innovationen im Angular-Umfeld.

Da hier im Unterbau von Angular einiges ausgetauscht wird, legt das Angular-Team ein besonderes Augenmerk auf Kompatibilität mit vorherigen Angular-Versionen: Bestehende Anwendungen sollen nach dem Umstellen auf Ivy genau so funktionieren wie zuvor und im besten Fall mit merkbar kleineren Bundles auskommen. Das ist nicht ganz uneigennützig, zumal bei Google offiziell über 600 Anwendungen auf Angular basieren – die „Dunkelziffer“, so wird gemunkelt, ist deutlich höher.

Mit Angular 8 steht nun Ivy in einer Vorab-Version zum Testen zur Verfügung. Das Ziel dieser Version ist es, frühes Feedback einzuholen. Deswegen empfiehlt das Angular-Team auch, für produktive Anwendungen noch nicht auf Ivy sondern auf die klassische View Engine zu setzen (Abbildung 1).

Abbildung 1: Bundlegrößen einer Hallo-Welt-Anwendung mit und ohne Ivy (entnommen aus: ngconf 2019 Keynote von Brad Green und Igor Minar)

Dank Differential Loading (siehe unten) lassen sich die Bundlegrößen damit bereits augenblicklich optimieren.

Wie von Brad Green, dem technischen Direktor hinter dem Angular-Team bei Google, auf der ngconf 2019 erwähnt, wird Ivy im Kompatibilitätsmodus gemeinsam mit Differential Loading eine merkbare Verbesserung der Bundlegrößen ermöglichen. Jene, die den Nervenkitzel suchen, können demnach auch schon heute das künftige API von Ivy ausprobieren. Gerade dieser Modus birgt ein beträchtliches Optimierungspotential. Dieses API ist jedoch derzeit noch als privat eingestuft, was man daran erkennt, dass deren Klassen und Funktionen mit dem Sonderzeichen ɵ beginnen.

Wer heute schon mal Ivy ausprobieren möchte, kann ein neues Projekt mit dem Schalter −−enable-ivy erzeugen:

 ng new ivy-project --enable-ivy

Das veranlasst das CLI, den folgenden Konfigurationseintrag in der tsconfig.app.json zu hinterlegen:

 
"angularCompilerOptions": { 
        "enableIvy": true 
}

Dieser Eintrag lässt sich nach dem Update auf Angular 8 auch manuell vornehmen, um eine bestehende Anwendung mit Ivy zu testen.

Zum Ausführen der Anwendung im Debug-Mode empfiehlt es sich, auf AOT zu setzen:

ng serve --aot

Außerdem lohnt sich ein Blick auf die Größe der mit ng build gebauten Anwendung. Mit Angular 9 soll Ivy schlussendlich zum ersten Mal standardmäßig aktiviert sein. Bis dahin möchte das Angular-Team durch weitere Maßnahmen die Komptabilität mit älteren Versionen sicherstellen.

Web Worker

JavaScript ist per Definition single-threaded. Deswegen ist es auch üblich, dass längere Aufgaben wie das Abrufen von Daten asynchron erfolgen. Das hilft natürlich nicht bei aufwändigen Berechnungen. Gerade die werden bei reichhaltigen JavaScript-Lösungen jedoch immer häufiger. Deswegen unterstützen mittlerweile so gut wie alle Browser Web Workers. Das sind Skripte, die der Browser in einem eigenen Thread ausführt. Die Kommunikation mit dem Thread im Browsertab erfolgt über das Versenden von Nachrichten.

Während Web Worker per se nichts mit Angular zu tun haben, gilt es sie jedoch beim Build zu berücksichtigen. Das Ziel ist es, pro Web Worker ein Bundle bereitzustellen. Genau diese Aufgabe übernimmt das neue Angular CLI.

iJS React Cheat Sheet

Free: React Cheat Sheet

You want to improve your knowledge in React or just need some kind of memory aid? We have the right thing for you: the iJS React Cheat Sheet (written by Joel Lord). Now you will always know how to React!

Um dieses neue Feature zu veranschaulichen, kommt eine JavaScript-Implementierung des sogenannten Damen-Problems, im Englischen auch als Queens Problem bekannt, zum Einsatz. Die Idee dahinter ist es, auf einem Schachbrett pro Reihe eine Dame zu platzieren, ohne dass sich diese gegenseitig schlagen können (Abb. 2). Das bedeutet, es dürfen sich keine anderen Damen in derselben Reihe, Spalte oder Diagonale befinden.

Abbildung 2: Eine mögliche Lösung des Damen-Problems (engl. Queens Problem)

Ein Algorithmus, der alle möglichen Lösungen auf einem Schachbrett ermittelt, gilt als rechenintensiv. Während die Berechnung für ein herkömmliches Schachbrett mit acht Reihen und acht Spalten noch relativ zügig von statten geht, stoßen handelsübliche Rechner ab 12 x 12 Felder an ihre Grenzen. Der aktuelle Rekord liegt in der Ermittlung der Lösungen für ein Brett mit 27 x 27 Felder. Hierfür kamen russische Supercomputer zum Einsatz.

Um eine solche Berechnung in den Hintergrund auszulagern, ist zunächst mit dem Angular CLI ein Web Worker zu generieren:

ng generate worker n-queens

Diese Anweisung erstellt nicht nur eine Datei für den Worker, sondern auch die für den Build-Vorgang nötigen Konfigurationsdateien sowie Einträge in bestehenden Dateien. Existiert im selben Ordner eine gleichnamige Komponente mit der üblichen Endung .component.ts, reichert das CLI diese Komponente sogar um Code an, der mit dem Web Worker kommuniziert.

Der Worker selbst besteht lediglich aus einem Event-Listener für das message-Event (Listing 1).

import nQueens from './n-queens';

addEventListener('message', ({ data }) => {
  const result = nQueens(data.count);
  postMessage(result, undefined);
});

Dieses Event kommt zur Ausführung, wenn der Haupt-Thread dem Worker eine Nachricht sendet. Der Parameter enthält die vom Hauptthread übersendeten Informationen. Im betrachteten Fall beschränken sich die auf die Eigenschaft count, welche die Schachbrettgröße bekannt gibt. Nach der Berechnung mit der aus Platzgründen hier nicht abgebildeten Funktion nQueens sendet der Event-Listener das Ergebnis mit postMessage an den Haupt-Thread zurück. Das bewirkt, dass der Browser dort ein message-Event auslöst.

Zur Interaktion mit diesem Worker-Script kommt in der nutzenden Komponente die Klasse Worker zum Einsatz (Listing 2).

const count = parseInt(this.count, 10);

const worker = new Worker('../logic/n-queens.worker', {
    type: 'module' // Worker uses EcmaScript modules
});

worker.postMessage({count});

worker.addEventListener('message', (event) => {
  // tslint:disable-next-line: no-console
  console.debug('worker result', event.data);

  // Update chessboard
  this.processResult(event.data);
});

Mit postMessage sendet die Komponente eine Nachricht mit der gewünschten Spielfeldgröße an den Worker und stößt somit dort die Berechnung an. Über das message-Event nimmt sie das Ergebnis entgegen.

In weiterer Folge kümmert sich das CLI um die korrekte Übersetzung und das Bundling der Worker-Skripte. Der dazu angestoßene TypeScript-Compiler erkennt diese an der Endung .worker.ts, die in der von ng generate worker erzeugen tsconfig.worker.json registriert ist. Damit das CLI diese Dateien nicht auch nochmal beim Übersetzen und Bundling der Hauptanwendung berücksichtigt, bringt ng generate worker dasselbe Dateimuster im Abschnitt exclude der tsconfig.app.json unter.

Die gesamte Implementierung befindet sich in der Beispielsammlung des Autoren. Zur Veranschaulichung ermöglicht das dort zu findende Beispiel das Lösen des Damenproblems sowohl im Haupt-Thread als auch in einem Web Worker. Fordert man beispielsweise eine Lösung für ein 12 x 12 Schachbrett an, stellt man fest, dass ersteres das UI einfrieren lässt, während die Hintergrundausführung im Worker die Bedienbarkeit während der Berechnung nicht einschränkt.

Differential Loading

Bis jetzt war es üblich, Anwendungen ins gute alte ECMAScript 5 zu kompilieren, da dieses „JavaScript unserer Väter“ heutzutage so gut wie überall läuft. Somit kann sowohl der IE 11 als auch der Web Crawler, der hinter der Google-Suchmaschine steht, den Programmcode ausführen.

Das neuere ECMAScript 2015 und seine nachfolgenden Versionen sind jedoch effizienter: Diese Versionen erlauben kompaktere Bundles und der Browser kann diese auch effizienter interpretieren. Da man sich bis dato mit ECMAScript 5 auf den kleinsten gemeinsamen Nenner zurückzog, konnten moderne Browser diese Vorteile leider nicht nutzen.

Damit ist jetzt Schluss: Das CLI bringt ab Version 8 ein Feature mit, das sich Differential Loading nennt. Die Idee davon ist, zwei Gruppen von Bundles bereitzustellen: Die einen basieren auf ECMAScript 5 und bedienen ältere Browser, die anderen basieren auf einer jüngeren ECMAScript-Version, z. B. ECMAScript 2015 und bieten modernen Browsern die genannten Vorteile.

Um Differential Loading zu aktivieren, muss gar nicht viel gemacht werden: Es gilt lediglich, eine obere als auch eine untere Schranke, für die zu unterstützende ECMAScript-Versionen festzulegen. Die obere Schranke ist in der tsconfig.json einzutragen:

    "target": "es2015"

Die untere Schranke definiert sich hingegen durch eine Browserslist. Das ist eine Datei die über bestimmte Kriterien, wie Marktverbreitung, eine Menge an Browsern identifiziert, die es zu unterstützen gilt. Diese sind z. B. in der Datei browserslist zu hinterlegen, welche das CLI mittlerweile beim Generieren eines neuen Projektes im Projektroot anlegt:

   
> 0.5%
last 2 versions
Firefox ESR
not dead
IE 9-11

Im betrachteten Fall verweist die browserlist unter anderem mit dem Eintrag IE 9-11 auf ECMAScript-5-Browser. Somit legt das CLI die untere Schranke auf diese Version fest.

Bekommt das CLI nun die Anweisung einen Build zu erstellen (ng build), finden Kompilierungs- und Bundlingvorgänge für beide Versionen statt, wie in Abbildung 3 zu sehen ist.

Abbildung 3: Build für Differential Loading

Der Nachteil dieses Vorgangs wird hier auch offensichtlich: Die nötige Zeit für den Build-Vorgang verdoppelt sich.

Die einzelnen Browser können nun entscheiden, welche Version der Bundles sie laden wollen. Dazu erhalten die script-Referenzen in der index.html Zusätze: Jene, die auf ECMAScript-5-Bundles verweisen, erhalten den Zusatz nomodule. Das verhindert, dass Browser, die ECMAScript-Module und somit min. ECMAScript 2015 beherrschen, die Referenz ignorieren. Die ECMAScrupt 2015+-Bundles bindet das CLI hingegen über type=“module“ ein. Ältere Browser ignorieren diese script-Tags somit:

   
  <script src="main-es2015.js" type="module"></script>
  <script src="main-es5.js" nomodule></script>

Im Gegensatz zu ng build nutzen alle anderen CLI-Befehle nur (!) die obere Schranke. Im betrachteten Fall ist das ECMAScript 2015. Das geschieht unter anderem aus Effizienzgründen: Gerade beim Debuggen oder Testen möchten Entwickler möglichst schnell ein Ergebnis sehen, ohne ständig auf einen zweiten Build warten zu müssen.

Lazy Loading

Der Router unterstützt seit den ersten Tagen von Angular Lazy Loading. Dazu wurde das zu ladende Modul bis dato über einen magic value identifiziert:

   
{
    path: 'lazy',
    loadChildren: () => './lazy/lazy.module#LayzModule'
}

Der Wert vor der Raute repräsentiert den Pfad, der zur Datei mit der Modulimplementierung führt; der Wert danach steht für die darin enthaltene Klasse. Diese Schreibweise funktioniert zwar mit Angular 8 auch noch, ist jedoch nun zugunsten von dynamischen ECMAScript-Importen deprecated:

   
{
    path: 'lazy',
    loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule)
}

Die neue Schreibweise beinhaltet zwar nach wie vor den Dateinamen als magic value. Da import jedoch von vielen IDEs unterstützt wird, liefern ungültige Werte augenblicklich einen Fehler.

Breaking Change bei ViewChild und ContentChild

Einen Breaking Change gibt es beim Einsatz von ViewChild und ContentChild, deren Verhalten in der Vergangenheit leider nicht konsistent war. Fragt in früheren Versionen eine Komponente damit ein Element an, dass sich in keiner strukturellen Direktive wie ngIf oder ngFor befand, stand das Abfrageergebnis bereits in ngOnInit zur Verfügung. Ansonsten konnte der Programmcode frühestens in ngAfterViewInit (bzw. ngAfterContentInit bei ContentChild) darauf zugreifen. Für Elemente, die aufgrund der Datenbindung erst zu einem späteren Zeitpunkt ins DOM eingelastet werden, musste der Programmcode ngAfterViewChecked bzw. ngAfterContentChecked einsetzen.

Da dieses Verhalten zu Verwirrung geführt hat, muss die Komponente nun angeben, wann die Auflösung zu erfolgen hat:

   
  @ViewChild('info', { static: false })
  paragraph: ElementRef;

Weist static den Wert true auf, versucht Angular das Element beim Initialisieren der Komponente zu finden. Das funktioniert nur, wenn es sich in keiner strukturalen Direktive befindet. Beim Einsatz von static: false findet die Auflösung nach dem Initiieren bzw. Aktualisieren der View statt.

Die Anweisung ng update versucht hier automatisch den richtigen Wert einzutragen. Ist das nicht möglich, platziert sie einen Kommentar mit einem TODO an dieser Stelle.

Abfragen mit den verwandten Dekoratoren ViewChildren und ContentChildren bleiben von dieser Änderung unberührt. Diese haben immer schon auf ein dynamisches Verhalten im Sinne von static: false gesetzt.

Neuerungen für ngUpgrade

Ein Problem beim Hybrid-Betrieb von AngularJS 1.x und Angular mit ngUpgrade bestand bis jetzt darin, dass sich gegebenenfalls die Router der beiden Frameworks um die URL duelliert haben. Das führte zu schwer nachvollziehbaren Nebeneffekten. Um das zu verhindern, existiert nun die Möglichkeit, ein und denselben Location-Service zum Zugriff auf die URL in beiden Framework-Versionen zu nutzen.

Dazu hat das Angular-Team die Möglichkeiten des Location-Services von Angular erweitert und damit einen Ersatz für $location in AngularJS bereitgestellt.

Aus diesem Grund existiert nun im Location-Service unter anderem eine neue Methode onUrlChange zum Überwachen von URL-Änderungen (Listing 3).

  
export class AppComponent {
  constructor(loc: Location, pLoc: PlatformLocation) {
    loc.onUrlChange((url) => console.debug('url change', url));
    console.debug('hostname: ', pLoc.hostname);
  }
}

Der PlatformLocation-Service bietet zusätzlich Zugriff auf die einzelnen Bestandteile der URL. Eine ausführliche Beschreibung, wie der darauf basierende $location-Ersatz zum besseren Verschränken der beiden Frameworks eingesetzt wird, findet sich in der Dokumentation. Außerdem findet sich da nun auch eine Idee zum Lazy Loading von AngularJS, welche auf den weiter oben erwähnten dynamischen ECMAScript-Importen basiert.

Fazit

Wieder einmal hat das Angular-Team Wort gehalten: Die Migration auf die neue Angular-Version ist einfach und geht ohne große Änderungen von Statten. Stattdessen gibt es einige sehr nette Abrundungen, die die Arbeit mit dem SPA-Framework aus der Feder von Google noch bequemer gestalten. Differential Loading schafft ein Bewusstsein dafür, dass sich die Bundlegrößen weiter optimieren lassen, wenn man alte Browser nicht oder zumindest mit separaten Bundles unterstützt. Die Web-Worker-Unterstützung zeigt, dass immer mehr Berechnungsintensive Aufgaben in den Browser wandern. Enthusiasten können nun auch erste Gehversuche mit Ivy machen.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

1 Kommentar auf "Angular 8 ist da: Ivy-Preview, Service Worker und Differential Loading – das sind die neuen Features"

avatar
400
  Subscribe  
Benachrichtige mich zu:
Daniel
Gast

Kleine Korrektur: ng generate worker n-queens should be ng generate webWorker

X
- Gib Deinen Standort ein -
- or -