Features, Features, Features

Angular 6: Die Neuerungen in Framework und CLI im Detail

Manfred Steyer

© Shutterstock.com / ojal

Angular Elements erlaubt das Bereitstellen von Web Components, und die noch experimentelle View Engine ngIvy verspricht konkurrenzlose kleine Bundles. Das CLI automatisiert lästige Aufgaben beim Einbinden und Aktualisieren von Bibliotheken und kann nun npm Packages bauen. Was gibt es sonst noch Neues in Angular 6?

Es ist wieder so weit: Mit Angular 6 steht eine neue Major-Version von Angular in den Startlöchern. Gleichzeitig geht auch ein neues Angular CLI an den Start, dessen Versionsnummer ab sofort mit der von Angular synchron laufen soll und daher auch direkt auf Version 6 springt.

Das Changelog enthält einige neue und richtungsweisende Features, aber auch viele Abrundungen wie beispielsweise einen zusätzlichen Schalter hier oder einen neuen Parameter dort. In diesem Artikel werden wir uns auf Ersteres konzentrieren und die neuen Möglichkeiten anhand von Beispielen kennenlernen, die wie immer in meinem GitHub-Account zur Verfügung stehen.

Angular im Videotutorial

Im entwickler.tutorial Angular – Eine Einführung erklärt Manfred Steyer alles, was man für einen professionellen Umgang mit Angular benötigt.

Web Components mit Angular Elements

Bis jetzt war Angular am besten für Gesamtlösungen geeignet. Wollte man jedoch lediglich bestehende Anwendungen um interaktive Bereiche erweitern, waren häufig andere Frameworks die erste Wahl. Ein Beispiel dafür sind statische Seiten, die von einem CMS gerendert werden und mit JavaScript-Widgets anzureichern sind.

Mit Angular Elements dringt Angular nun auch in diesen leichtgewichtigen Bereich vor, indem es Angular-Komponenten als Web Components bereitstellt. Genau genommen muss man von Custom Elements sprechen, zumal Web Components ein Sammelbegriff für verschiedene Technologien ist. Egal wie man das Kind auch nennt, unterm Strich geht es um frameworkunabhängige Komponenten, die sich wie Standard-HTML-Elemente verhalten. Hat man das dafür verantwortliche npm-Paket @angular/elements installiert, kann eine beliebige Angular-Komponente mit der Methode createCustomElement in ein Custom Element umgewandelt werden (Listing 1).

[...]
import { createCustomElement } from '@angular/elements';
 
@NgModule({
  imports: [ CommonModule ],
  declarations: [FlightCancellingComponent],
  entryComponents: [FlightCancellingComponent]
})
export class FlightCancellingModule { 
   
  constructor(injector: Injector) {
    const ngElementConstructor = createCustomElement(FlightCancellingComponent, {
      injector
    });
 
    customElements.define('flight-cancelling', ngElementConstructor);
  }  
}

Um das so geschaffene Element mit dem Dependency-Injection-Mechanismus von Angular zu verbinden, übergibt das gezeigte Beispiel auch den aktuellen Injector. Die Methode customElements.define, die das Element beim Browser registriert und ihm einen Tagnamen zuweist, ist bereits Bestandteil der Custom-Elements-Spezifikation.

Da die dahinterstehende Angular-Komponente bei Bedarf dynamisch erzeugt wird, ist sie unter entryComponents einzutragen. Das ist notwendig, damit sie der Angular-Compiler entdeckt, obwohl sie zum Zeitpunkt der Kompilierung nicht mit den anderen Angular-Komponenten verbunden ist.

Wo und wann man das Custom Element erzeugt sowie registriert, ist unerheblich. Sobald die dazu notwendigen Anweisungen gelaufen sind, kann die Anwendung unabhängig vom Framework das neue HTML-Element verwenden. Ein Einsatz innerhalb von Angular könnte so aussehen:


Alternativ dazu lassen sich Custom Elements auch zur Laufzeit dynamisch zu einer Seite hinzufügen (Listing 2).

@Injectable()
export class SimpleFlightCancellingService  {
 
  show(flightId: number): void {
    const flightCancelling = document.createElement('flight-cancelling');
 
    flightCancelling['flightId'] = flightId;
    flightCancelling.addEventListener('closed', () => document.body.removeChild(flightCancelling));
 
    document.body.appendChild(flightCancelling);
  }
}

Hierzu kommen lediglich DOM-Bordmittel zum Einsatz. Sie sind im Gegensatz zur dynamischen Erzeugung von Angular-Komponenten sehr geradlinig und gestalten solche Szenarien um einiges einfacher.

Ein weiteres Einsatzgebiet für Angular Elements stellt die Orchestrierung von Micro-Apps dar. Dahinter steckt die Idee, anstatt einer großen Anwendung mehrere kleinere zu erstellen. Das soll unter anderem die Komplexität vermindern und die Wartbarkeit erhöhen. In Form von Custom Elements lassen sich solche Apps dynamisch sehr einfach in eine Rahmenanwendung laden und dem Benutzer als ein großes Ganzes präsentieren.

Ein Problem von Angular Elements ist derzeit, dass jede noch so kleine Komponente von Angular abhängig ist. Dies bläht die Bundles natürlich unnötig auf. Kommt in der Anwendung sowieso Angular zum Einsatz, ist das kein Problem. Muss man aber nur wegen simpler Custom Elements große Teile von Angular ins Bundle aufnehmen, führt das zu einem unnötigen Overhead. Abhilfe schafft hier die neue View Engine ngIvy, die im nächsten Abschnitt vorgestellt wird.

Zukunftsmusik: kleinere Bundles dank ngIvy

Die meisten Ressourcen bei der Umsetzung von Angular 6 sind in ein sehr innovatives Konzept geflossen: die neue View Engine ngIvy. Sie kompiliert Angular-Templates zu äußerst DOM-nahem Code und ist für Tree Shaking optimiert. Das bedeutet, dass nur die Teile von Angular im Bundle landen, die auch tatsächlich benötigt werden. Möchte man zum Beispiel lediglich einfache Custom Elements mit Angular bereitstellen, lässt sich Angular fast vollständig wegsprengen. Die damit erzielbaren Resultate sind durchaus beeindruckend: Wie das Angular-Team auf der ng-conf 2018 mitgeteilt hat, lässt sich eine einfache Hallo-Welt-Anwendung damit auf beeindruckende 2,7 KB schrumpfen, wenn man sie komprimiert überträgt.

Das Beste an ngIvy ist, dass Anwendungsentwickler davon bis auf kleinere Bundle-Größen nichts bemerken sollen. Es handelt sich um einen Austausch einer Implementierung unter der Motorhaube. Diese angestrebte Abwärtskompatibilität wird für eine der nächsten Angular-Versionen, idealerweise Version 7 im Herbst, angestrebt. Bis dahin handelt es sich bei ngIvy um ein experimentelles Feature, das hinter einem Schalter versteckt ist.

Treeshakable Providers

Obwohl das Angular-Team von Anfang an auf Tree Shaking zur Reduktion der Bundle-Größen setzte, waren Services bis jetzt nicht „treeshakable“. Das bedeutet, dass immer alle Services der Anwendung, aber auch jene von Bibliotheken im Bundle gelandet sind – egal, ob sie verwendet wurden oder nicht. Der Grund dafür liegt in der Art und Weise, wie Provider für Services deklariert werden (Abb. 1).

Da Provider auf Modulebene definiert werden, besteht immer ein indirekter Verweis zwischen der Anwendung und dem Service, selbst dann, wenn diese den Service nicht referenziert. Deswegen entschieden sich Tree-Shaking-Algorithmen bisher gegen ein Entfernen der Services.

Die Lösung, die das Angular-Team dafür gefunden hat, ist vom Prinzip her sehr einfach: Man dreht einfach einen der Pfeile um (Abb. 2). Durch diese Änderung verweist das Modul nicht mehr auf den Service, sondern der Service auf das Modul. Somit können Werkzeuge erkennen, ob der Service von der Anwendung verwendet wird. Im Code sieht das dann so aus:

@Injectable({ provideIn: 'root')
export class SimpleFlightCancellingService  { [...] }

Die Eigenschaft provideIn gibt das Modul bekannt, für dessen Scope der Service einzurichten ist. In den meisten Fällen wird man hier mit der String-Konstante root vorliebnehmen. Dabei handelt es sich nicht nur um den Scope des Hauptmoduls, sondern auch um den aller weiteren Module, die gemeinsam mit dem Hauptmodul in den Browser geladen werden. Lediglich per Lazy Loading bezogene Module erhalten ihren eigenen Scope.

In Fällen, in denen eine Indirektion zwischen dem angeforderten Token und dem tatsächlich zu injizierenden Service benötigt wird, sind weitere Eigenschaften hinzuzuziehen:

@Injectable({
  providedIn: 'root',
  useClass: LazyFlightCancellingService,
  deps: [NgModuleFactoryLoader, Injector]
})
export class FlightCancellingService { }

Neben useClass lassen sich hier auch die anderen bekannten Eigenschaften nutzen: useValue, useFactory und useExisting.

Whitespaces entfernen

Eine weitere Möglichkeit zur Optimierung der Bundle-Größen ist das Entfernen von Whitespaces aus den Templates; sie machen bei vielen Anwendungen 7 bis 13 Prozent der Größe aus. Angular beherrscht diese Aufgabe zwar schon länger, allerdings musste man sie bis dato explizit aktivieren. Ab Version 6 wird sie standardmäßig aktiviert sein. Im sehr unwahrscheinlichen Fall, dass man die Whitespaces doch benötigt, kann der Schalter PreserveWhitespaces in der tsconfig.json bzw. tsconfig.app.json bei CLI-Projekten genutzt werden:

"angularCompilerOptions": {
  "preserveWhitespaces": true
}

Dieselbe Option steht auch für einzelne Komponenten im Dekorator @Component zur Verfügung.

Architect: Auf dem Weg zu Bazel und Co.?

Zum Schnüren von Bundles setzt das Angular CLI auf die populäre Lösung webpack. Um künftig auch alternative Werkzeuge unterstützen zu können, hat das CLI-Team mit dem Projekt Architect ein Plug-in-System geschaffen. Während die standardmäßig mit dem CLI ausgelieferten Plug-ins an webpack delegieren, kann jeder nun auch andere Werkezeuge integrieren. Welche Plug-ins für welche Aufgaben zum Einsatz kommen, ist in die Steuerungsdatei angular.json einzutragen, die die bisherige .angular-cli.json ersetzt. Damit muss man sich in der Regel jedoch nicht belasten, da sich das CLI beim Generieren des Projekts darum kümmert.

Es gilt als wahrscheinlich, dass das CLI-Team diesen Mechanismus nutzt, um künftig das hausinterne Bazel als optionales Build-Werkzeug zu integrieren. Dieses im Gegensatz zu webpack sprachneutrale Werkzeug verspricht eine durchgängige Lösung, mit der sich auch serverseitige Anwendungen bauen lassen. Außerdem verspricht Bazel selbst bei riesigen Anwendungen ein extrem schnelles Verhalten, da es konsequent auf inkrementelles Kompilieren setzt. Aber auch andere Spielarten fürs Bundling lassen sich somit einklinken. Ein Beispiel dafür ist eine Spielart, die serverseitiges Rendering berücksichtigt.

Ein anderes lang erwartetes Feature basiert ebenfalls auf Architect: Die Möglichkeit, npm-Pakete zu generieren, wird im nächsten Abschnitt vorgestellt.

npm-Pakete mit dem neuen CLI

Zur Schaffung von wiederverwendbaren Bibliotheken bieten sich in der JavaScript-Welt npm-Pakete an. Sie lassen sich versionieren und über eine hausinterne oder öffentliche Registry verteilen. Bis jetzt musste man zum Erzeugen solcher Pakete auf Communityprojekte zurückgreifen. Künftig übernimmt das CLI auf Wunsch diese Aufgabe. Dazu fügt man zu einem bestehenden CLI-Projekt einfach eine Bibliothek hinzu:

ng generate library mylib

Zusätzlich bietet es sich an, eine Demoanwendung zum Testen zu generieren:

ng generate application playground-app

Solche zusätzlichen Bibliotheken und Anwendungen landen im Ordner projects (Abb. 3).

Die Standardanwendung befindet sich wie gewohnt unter src. Wer sie nicht benötigt, kann diesen Ordner auch löschen. Zusätzlich gilt es dann, auch den entsprechenden Eintrag aus der angular.json zu entfernen. Die Anweisung ng build --prod baut sämtliche Bestandteile des Projekts. Für Libraries delegiert das CLI mittels Architect an ng-packgr weiter. Dabei handelt es sich um eine populäre Communitylösung zum Generieren von npm-Paketen für Angular. Nach dem Build-Vorgang findet man die Bibliothek unter dist/mylib und kann sie mit npm publish veröffentlichen. Für den hausinternen Einsatz bietet diese Anweisung den Schalter –registry, mit dem sich eine private npm Registry verwenden lässt.

Damit die Playground-App die Library findet, trägt das CLI ein Mapping in die Datei tsconfig.json ein. Dieses lässt sich an eigene Bedürfnisse anpassen:

"paths": {
  "@my/logger-lib": [
    "projects/logger-lib/src/public_api.ts"
  ]
}

Dieser Eintrag bewirkt, dass alle TypeScript-Importe, die @my/logger-lib adressieren, auf die Quellcodedateien der Bibliothek verweisen. Nach dem Build könnte man diesen Eintrag umstellen, sodass er auf die Kompilate im dist-Order verweist. Entfernt man den Eintrag, geht TypeScript davon aus, dass sich die Bibliothek neben anderen installierten Paketen im Ordner node_modules befindet. Somit lassen sich verschiedene Stadien der Bibliothek einfach testen. Als Alternative zum ständigen Modifizieren der tsconfig.json bietet sich der Einsatz mehrerer tsconfig.json-Dateien an. Die jeweils zu nutzende ist in diesem Fall an ng build zu übergeben.

Die hier aufgezeigte Möglichkeit ist jedoch nicht nur für diejenigen interessant, die npm-Pakete bauen wollen. Sie bietet sich auch an, um große Angular-Projekte zur besseren Wartbarkeit in kleinere Subprojekte zu unterteilen. Wem diese Idee gefällt, wird auch das auf das CLI aufbauende Projekt Nx mögen, das für solche Szenarien zusätzliche Werkzeuge und Codegeneratoren bietet.

Lazy Loading ohne Router

Lazy Loading ist gerade für große Anwendungen ein wichtiges Feature: Anstatt alles beim Programmstart zu laden, holt man sich zu Beginn nur die wichtigsten Module in den Browser, den Rest lädt die Anwendung bei Bedarf nach. Zum Einsatz von Lazy Loading bietet der Router ein sehr komfortables API, das die weniger handlichen Low-Level-APIs, die Lazy Loading erst ermöglichen, verbirgt.

Doch Lazy Loading zur Laufzeit zu unterstützen, ist nur eine Seite der Medaille; auch die Build-Werkzeuge müssen mitspielen und für einzelne Bereiche Bundles erzeugen. Diese Aufgabe hat das CLI seit seiner ersten Version auch fest im Griff: Es untersucht den Quellcode nach entsprechenden Routing-Einträgen und splittet zusätzliche Bundles ab. In Fällen, in denen Anwendungsentwickler jedoch zugunsten von mehr Kontrolle auf die Low-Level-APIs zurückgreifen wollen, bot das CLI bis dato keine Unterstützung. Das ändert sich nun mit Version 6; hier lassen sich Module in die Steuerdatei angular.json eintragen, die in eigene Bundles auszulagern sind:

"options": {
  [...],
  "lazyModules": [
    "src/app/flight-cancelling/flight-cancelling.module"
  ]
}

Solche Module lassen sich anschließend mit dem NgModuleFactoryLoader von Angular zur Laufzeit nachladen. Um seine Verwendung zu veranschaulichen, lädt der Service in Listing 3 ein Modul mit einem Angular-Element.

@Injectable()
export class LazyFlightCancellingService extends SimpleFlightCancellingService {
 
  constructor(
    private loader: NgModuleFactoryLoader,
    private injector: Injector
  ) {
      super();
  }
 
  private moduleRef: NgModuleRef;
 
  show(flightId: number): void {
 
    if (this.moduleRef) {
      super.show(flightId);
      return
    }
 
    const path = 'src/app/flight-cancelling/flight-cancelling.module#FlightCancellingModule'
    this
      .loader
      .load(path)
      .then(moduleFactory => {
        this.moduleRef = moduleFactory.create(this.injector).instance;
        console.debug('moduleRef', this.moduleRef);
 
          // Geerbte Basisimplementierung erzeugt Element
          super.show(flightId);
      })
      .catch(err => {
        console.error('error loading module', err); 
      });
  }
}

Interessant ist hierbei, dass die NgModuleRef-Instanz, die das geladene Module beschreibt, gar nicht weiter verwendet wird. Es reicht, dass das Modul das Angular-Element einrichtet (Listing 1). Ab dann kann es, wie jedes andere HTML-Element auch, mit document.createElement erzeugt werden. Hierzu erbt dieser Service vom Service in Listing 2, der sich um genau diese Aufgabe kümmert.

Wie auch bei Modulen, die über den Router nachgeladen werden, gilt, dass sie nicht von einem beim Programmstart geladenen Modul importiert werden dürfen. In diesem Fall landet nämlich das Modul im Haupt-Bundle was wiederum Lazy Loading ad absurdum führt.

Bibliotheken mit Schematics, ng-add und ng-update einreichen und aktualisieren

Eine sehr monotone und somit auch lästige Aufgabe ist das Einrichten von Paketen, die man in eine JavaScript-Anwendung lädt. Denken wir zum Beispiel an die Bibliothek Angular Material: Nach dem Beziehen mittels npm install muss man die richtigen Angular-Module aus diesem Paket importieren, eine Stylesheetdatei einbinden und das Grundgerüst der Anwendung aus einzelnen Komponenten zusammensetzen. Um solche Aufgaben zu automatisieren, nutzt das CLI seit einiger Zeit das Scaffolding-Werkzeug Schematics. Jede Bibliothek kann damit eigene Generatoren für Standardaufgaben anbieten. Diese werden mit dem bekannten Befehl ng generate angestoßen.

Daneben bietet das CLI nun auch zwei weitere Befehle: ng add und ng update. Ersterer bezieht ein Paket mittels npm oder Yarn und ruft einen darin enthaltenen Schematic mit dem Namen ng-add auf. Dieser kann sich gleich an Ort und Stelle um das Einrichten der Bibliothek kümmern. Die Anweisung ng update bezieht die neueste Version einer Bibliothek und führt definierte Update-Schematics aus, um die bestehende Anwendung zu aktualisieren. Listing 4 demonstriert den Einsatz dieser Möglichkeiten.

npm install @angular/service-worker
ng add @angular/pwa
 
ng add @angular/material
ng add @angular/cdk
 
ng generate @angular/material:material-table --name table
ng generate @angular/material:material-nav --name nav
ng generate @angular/material:material-nav --name dashboard

Dieses Beispiel bezieht mit ng add das neue Paket @angular/pwa, das mit Schematics aus der Angular-Anwendung unterstützt durch @angular/service-worker eine offlinefähige Progressive Web App macht. Auf dieselbe Weise wird Angular Material eingerichtet. Die Anweisung ng generate generiert danach den häufig benötigten Boilerplate, der auf Angular Material basiert. Ruft man diesen Boilerplate in der Anwendung auf, sieht die auf diesem Weg erhaltene Progressive Web App wie in Abbildung 4 aus.

Für die Aktualisierung auf die neuesten Angular-, CLI- und Material-Versionen wird man künftig auch auf ng update zurückgreifen können:

npm install -g @angular/cli
npm install @angular/cli
ng update @angular/cli
ng update @angular/core

Beim Betrachten dieser Zeilen fällt auf, dass das CLI direkt per npm install bezogen wird. Das ist bei der Migration auf Version 6 notwendig, um ein Henne/Ei-Problem zu lösen, denn zunächst muss man erst einmal eine Angular-Version haben, die ng add und ng update unterstützt.

Breaking Changes und Long-Term-Support-Version

Auch wenn ein Major-Release prinzipiell Breaking Changes beinhalten kann, ist die Weiterentwicklung von Angular sehr evolutionär. Das Team legt viel Wert auf Rückwärtskompatibilität: Neue Features wie beispielsweise ngIvy werden zunächst einmal als experimentell eingestuft und hinter einem Flag versteckt. Wer beim Update auf Probleme stößt, findet im Update Guide eine Schritt-für-Schritt-Anleitung, um von der aktuell verwendeten Version auf die neueste zu kommen. Gerade bei der Abhängigkeit RxJS hat es ein paar Bereinigungen gegeben, aber die lassen sich mit den dort verlinkten Informationen zum Großteil automatisiert bzw. mit einem Shim berücksichtigen.

Prinzipiell empfiehlt das Angular-Team, weitestgehend die neuesten Versionen zu verwenden. Wie einige der Ausführungen hier gezeigt haben, bringt das häufig auch merkliche Performanceverbesserungen, da das Produktteam ständig Optimierungen unter der Motorhaube durchführt.

Für alle, die nicht ständig auf die neuesten Versionen wechseln können, gibt es Long-Term-Support-Versionen. Und auch hierzu hatte das Produktteam auf der ng-conf 2018 eine erfreuliche Ankündigung: Künftig wird es pro Version – und nicht wie zuvor geplant nur für jede zweite Version – Long-Term-Support geben. Er erstreckt sich über zwölf Monate und beginnt mit der Veröffentlichung der jeweils nächsten Version. Das bedeutet, dass jede einzelne Version von Angular über 18 Monate hinweg unterstützt wird und sicherheitsrelevante Updates und Bugfixes bekommt.

Fazit

Version 6 beweist einmal mehr, wie solide das Fundament von Angular ist. Beispielsweise erlaubt es Initiativen wie ngIvy, die die gesamte View Engine ausgetauscht. Durch ein bloßes Update auf die neueste Version erhalten bestehende Anwendungen somit merkbare Performanceverbesserungen. Die veröffentlichten 2,7 KB, die derzeit eine darauf basierende Hallo-Welt-Anmeldung benötigen, ist mehr als konkurrenzfähig. Neue Möglichkeiten wie treeshakable Providers oder Whitespace Removal schlagen in dieselbe Kerbe und zeigen, dass Performance auf der Liste der Architekturziele ganz oben steht.

Durch die Kombination von ngIvy und Angular Elements dringt Angular nun in Bereiche vor, die zuvor von kleineren Frameworks dominiert wurden. Mit Angular Elements löst das Team auch sein ursprüngliches Versprechen ein, auf Web Components setzen zu wollen.

Auf der anderen Seite hilft das CLI immer stärker, den Einsatz von Angular so einfach wie möglich zu machen. Dank Architect können neue Build-Werkzeuge wie ng-packagr zum Bauen von Bibliotheken eingeklinkt werden, und Schematics hilft beim Einrichten, Verwenden, aber auch Aktualisieren von Bibliotheken, indem es monotone und somit auch fehleranfällige Aufgaben automatisiert.

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

1 Kommentar auf "Angular 6: Die Neuerungen in Framework und CLI im Detail"

avatar
400
  Subscribe  
Benachrichtige mich zu:
Reiner
Gast

Sechs Angular-Artikel auf der ersten Seite. Das haben nicht mal MongoDB oder Microservices geschafft