Road to Angular 9
Seit dem Angular-2-Release im September 2016 ist die Qualität und der Umfang des Angular-Frameworks beständig gewachsen. Gerade die Versionen 7, 8 oder auch 9 erwecken auf den ersten Blick den Eindruck, dass es gar nicht so viel Neues gibt. Blickt man auf die Änderungen im Detail, stellt man fest, dass sich doch eine ganze Menge getan hat. Beginnen wir mit einem Blick auf die Historie.
Historie
Das Vorgänger-Framework Angular JS, das im Jahr 2009 veröffentlicht wurde, hat die damals aktuellen Technologien genutzt. Mit fortschreitender Entwicklung im Web und dem Wunsch, größere und komplexere Singlepage-Applications zu schreiben, zeigten sich immer deutlicher Grenzen.
Angular 2
Im März 2016 startete die Release-Candidate-Phase von Angular 2. Abseits vom Namen haben beide Frameworks nicht mehr viel gemeinsam. Die Benutzungsoberfläche wird in Form von Komponenten, die auch ineinander geschachtelt sein können, realisiert. Eines der großen Probleme von Angular JS war die Art und Weise der Change Detection. Mit der Einführung von ZoneJS in Angular wurde dieser Punkt massiv verbessert. Zur Behandlung von Events wurde und wird immer noch RxJS eingesetzt. Entwickelt wird Angular mit dem Javascript Superset Typescript. Das Hinzufügen von statischer Typisierung ermöglicht die Entwicklung robuster und gut wartbarer Anwendungen.
Angular 4
Aufgrund unterschiedlicher interner Versionsnummern hat sich das Angular-Team dazu entschieden, die Version 3 zu überspringen und direkt die Version 4 zu veröffentlichen. Eine wichtige Neuerung in Version 4 war die Einführung des HttpClient-Service. Anders als der Http-Service trifft der HttpClient viele sinnvolle Annahmen, über die Requests, die versendet werden. Zusätzlich ist es mit dem HttpClient möglich, über Generics dem Typescript-Compiler mitzuteilen, welches Ergebnis erwartet wird.
Angular 5
In Angular 5 fanden viele Verbesserungen am Angular CLI statt. Es wurde zum Beispiel der Build Optimizer eingefügt, der die Bundle Size reduziert. Auch ist das Zusammenspiel für das Server Side Rendering mit der State Transfer API verbessert worden. Wird die Anwendung serverseitig gerendert, können die Ergebnisse der Daten-Abrufe mittels der State Transfer API gespeichert werden. Auf diesem Weg wird, wenn die Anwendung im Browser gestartet wird, ein erneuter Abruf der Daten unnötig.
Angular 6
In der Version 6 wurde die Abhängigkeit von RxJS 5 auf RxJS 6 geändert. Ein Effekt der Umstellung war es, dass die Operatoren der Observable besser treeshakeable geworden sind. Dank des neuen Update-Schematic war das Update für sehr viele Anwender sehr einfach umzusetzen. Jede Bibliothek kann einen eigenen Update-Pfad mitbringen und Updates automatisieren. Voraussetzung dafür ist es, sich an die Standards zu halten, die vom Angular CLI festgelegt werden. Statt Tage oder Wochen dauern Angular-Updates so im besten Fall nur noch Minuten.
Angular 7
Die Version 7 des Frameworks brachte viele Veränderungen unterhalb der Oberfläche. Die Performance ist verbessert worden. Mit den an Ivy begonnen Arbeiten wurde der Angular Compatibility Compiler (ngcc) eingeführt. Dieser wird notwendig, um Komponenten, die nicht für Ivy kompiliert worden sind, für Ivy fit zu machen. Am Angular CLI wurden weitere Verbesserungen vorgenommen. Dazu zählen auch die neuen Prompts des CLI, die benötigte Angaben interaktiv vom Entwickler einfordern können.
Angular 8
Das Angular CLI ist in der Version 8 um den Deploy Builder erweitert worden. Mit diesem Builder kann das Deployment automatisiert werden. Um die Bundle Size weiter zu optimieren, wurde das Differential Loading im Angular CLI Build integriert. Dabei werden, abhängig von den Browsern, die unterstützt werden sollen, mehrere Bundles erzeugt. Für Legacy-Browser wird wie gewohnt ein Bundle mit Ecmascript 5 erzeugt. Für moderne Browser wird zusätzlich ein Bundle mit Ecmascript 2015 erzeugt. Das Verhalten von ViewChild und ContentChild war bis einschließlich Angular 7 nicht immer verständlich. Mit dem verpflichtendem Flag static wird der Entwickler gezwungen, das Verhalten explizit anzugeben. Ivy wird von großen Teilen der Community lange erwartet. Mit Angular 8 kann mittels des Opt-In Ivy ausprobiert werden.
Angular 9
Auf den ersten Blick könnte man den Eindruck gewinnen, dass es nicht viele Neuerungen gibt. Die Arbeiten an Ivy wurden weiter vorangetrieben. Zusätzlich ist ein Opt-In für das Build System Bazel verfügbar.
ViewChild und ContentChild
Beide Decorators ermöglichen das Abfragen von Kindern der Komponente. Mittels ViewChild werden Kinder selektiert, die als Kinder innerhalb des Templates der Komponente enthalten sein können. Im Gegensatz dazu kann ContentChild genutzt werden, um Abfragen im Kontext des ng-content-Tags zu starten. In der Regel stehen die Ergebnisse der Abfragen in der AfterViewInit-Phase des Lifecycle zur Verfügung. Bisher kam es vor, dass das Ergebnis der Abfrage schon in OnInit zur Verfügung stand. Die Entscheidung darüber hat Angular automatisch getroffen. Mit Angular 8 wird der Entwickler gezwungen, das Verhalten selbst auszuwählen und explizit als Option an ViewChild bzw. ContentChild zu übergeben. Die Option ist das Flag static. Nur wenn der Wert auf false gesetzt wird, werden bei der Abfrage die Ergebnisse von Bindings und Direktiven (ng-for, ng-if, usw.) mit einbezogen. Die Werte stehen frühestens in AfterViewInit zur Verfügung. Dies bildet die meisten Anwendungsfälle ab und ist die Empfehlung des Angular-Teams. In seltenen Fällen, in denen die Query etwa eine Abhängigkeit auf die TemplateReference beinhaltet, muss das static-Flag auf true gesetzt werden. Dann steht das Ergebnis schon in OnInit bereit, wird aber auch nur genau einmal ausgewertet.
Ab Angular 9 wird dieser Parameter wieder optional und ist per default auf static: false gesetzt. Da mit dem Update auf Angular 8 der Parameter verpflichtend war und von Entwicklern angegeben werden musste, kann mit Angular 9 das Standard-Verhalten ohne Parameter geändert werden, ohne ein Breaking Change zu verursachen.
Dynamic Imports
Die Syntax für das Lazy Loading ist um die Möglichkeit von Dynamic Imports erweitert worden. Bei der Routen-Definition wird der Pfad zu dem lazy zu ladenden Modul in Form eines "Magic Strings" angegeben. In diesem steht – relativ zur Datei, in dem die Route regestriert wird – der Pfad zu der Datei, die das zu ladende Modul enthält. Gefolgt wird der Pfad von einer Raute und dem Namen der Klasse des Moduls. Im Ecmascript-Standard 2020 werden Dynamic Imports eingeführt. Dank Typescript kann dies auch schon jetzt benutzt werden. Dabei wird die globale Funktion import aufgerufen, die ein Promise auf den Inhalt der Datei angibt. Dort kann dann auf den benannten Export typsicher zugegriffen werden.
// magic string
{ path: 'search', loadChildren: './search/search.module#SearchModule' },
// dynamic import
{ path: 'search', loadChildren: () =>
import('./search/search.module').then(m => m.SearchModule) }
Schematics
Schematics sind ein wichtiger Bestandteil des Angular CLI. Aufgaben zur Generierung von Code und auch der Änderungen an bestehendem Code werden vom CLI an die Schematics delegiert. Ein prominentes Beispiel, das fast jeder Entwickler nutzt, ist der Befehl ng generate schematicName. So können Aufgaben, vor denen Entwickler stehen, automatisiert werden. Hierzu zählen auch Aufgaben wie die notwendigen Schritte zum Hinzufügen von Bibliotheken. Dazu kann der Befehl add im Angular CLI genutzt werden, wie zum Beispiel ng add @angular/material. Dabei sind Entwickler nicht auf die Schematics beschränkt, die vom Angular CLI mitgeliefert werden. Es können auch Schematics selbst geschrieben werden, oder aber auch per Abhängigkeit installiert werden.
Angular Builders
Ähnlich den Schematics werden vom Angular CLI die Vorgänge zum Bauen der Anwendung an die Builders delegiert. Mittels der Konfiguration aus der Datei angular.json und optional weiteren JSON-Dateien wird der Build-Schritt durchgeführt. Dazu werden in der Regel Kind-Prozesse die zum Beispiel Tools per Kommandozeile aufrufen. Neu eingeführt wurde mit der Version 8.3.0 der Deploy-Builder. Er ermöglicht das automatisierte Deployment direkt aus dem Angular CLI. Für einige Anbieter (Azure, Firebase, Github-Pages, usw.) gibt es schon fertige Deploy-Builder.
Differential Loading
Moderne Browser sind in der Lage, Dateien in der Syntax von Ecmascript 2015 und höher zu verarbeiten. Das Transpellieren von Code zu Ecmascript 5 ist nicht immer notwendig. Dem trägt Angular seit der Version 8 Rechnung. Über die browserslist werden mittels einer Query die Browser ermittelt, die unterstützt werden sollen. Auf dieser Grundlage wird dann vom Angular CLI entschieden, ob ein Build für Ecmascript 5 erforderlich ist. In diesem Fall wird ein zusätzlicher Build gestartet und zwei Bundle werden erzeugt, – eins mit dem Target Ecmascript 5 und eins für Ecmascript 2015. Dabei werden auch Polyfills durch das Angular CLI eingebunden.
Ivy
Ivy besteht aus zwei Teilen. Der erste Teil ist ein neuer Ahead of Time Compiler für Angular-Templates. Der zweite Teil ist eine Rendering Engine, die die View Engine ablösen wird. Mit der Version 8 ist ein Opt-In für Ivy verfügbar und soll mit Angular 9 der Standard werden.
Ziele von Ivy
- Beibehaltung der Kompatibilität
- Bessere Optimierbarkeit (Treeshaking etc)
- Größere Flexibilität
- Inkrementelles Kompilieren
- Lokalität
Ahead of Time Compiler
Die HTML-Templates, die der Entwickler schreibt, werden durch Angular nicht direkt verwendet. Sie werden über einen Compiler geparsed und in ausführbaren Javascript-Code übersetzt. Sofern nicht anders konfiguriert, werden die Templates in einem produktiven Build vorab zum Zeitpunkt des Build-Vorgangs übersetzt. Die Art des Codes der von Ivy generiert wird, unterscheidet sich sehr zu dem vorherigen Template Compiler.
Der vorherige Ahead of Time Compiler erzeugt aus den HTML-Templates Code, der dann von der View Engine interpretiert wird. Gerade die Interpretation sorgt dafür, dass der Framework-Code von Angular nicht dem Tree Shaking unterzogen werden kann, da erst zur Laufzeit entschieden wird, welche Funktionalitäten des Frameworks tatsächlich verwendet werden. Zum anderen müssen auch die Templates von Bibliotheken, die als Abhängigkeit installiert werden, während des Build-Vorgangs durch den Angular Compiler übersetzt werden.
Gegenüberstellung View-Engine und Ivy
// out-tsc/app/src/app/movies/movie-tile/movie-tile.component.ngfactory.js
export function View_MovieTileComponent_0(_l) {
return i1.ɵvid(0, [i1.ɵpid(0, i2.TruncatePipe, []),
(_l()(), i1.ɵeld(1, 0, null, null, 28, "mat-card", [["class", "mat-card"]], null, null, null, i3.View_MatCard_0, i3.RenderType_MatCard)),
// …
}
// out-tsc/app/src/app/movies/movie-tile/movie-tile.component.js
MovieTileComponent.ngComponentDef = i0.ɵɵdefineComponent({ type: MovieTileComponent, selectors: [["tcc-movie-tile"]], factory: function MovieTileComponent_Factory(t) { return new (t || MovieTileComponent)(i0.ɵɵdirectiveInject(i1.TmdbService)); }, inputs: { movie: "movie", routerPrefix: "routerPrefix" }, consts: 14, vars: 14, template: function MovieTileComponent_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "mat-card");
i0.ɵɵelementStart(1, "mat-card-header");
i0.ɵɵelementStart(2, "mat-card-title", _c0);
i0.ɵɵelementStart(3, "h2");
i0.ɵɵtext(4);
i0.ɵɵelementEnd();
i0.ɵɵelementEnd();
i0.ɵɵelementEnd();
Inkrementelles Rendering
Da in den Template Instructions nur die Public-API der Abhängigkeiten verwendet wird kann, solange diese stabil bleibt, kann davon ausgegangen werden, dass die Abhängigkeit aktuell ist. Oder anders herum, nur bei der Änderung an der öffentlichen Schnittstelle müssen die Abhängigkeiten neu kompiliert werden. Damit wird es erstmals möglich, vorkompilierte Komponenten, Direktiven, Pipes und so weiter zu nutzen.
Durch React wurde der Begriff des Virtual DOM geprägt. Der Virtual DOM ist eine programmatische Repräsentation des Browser DOM. Der Virtual DOM wird dabei als ganzes überprüft und bei Änderungen wird der Browser DOM transformiert, bis er wieder mit dem Virtual DOM übereinstimmt. Mit Ivy hat das Angular-Team den Begriff des Incremental DOM eingeführt. Die Template Instructions beinhalten die Anweisungen, wie der Incremental DOM in den Browser DOM überführt werden kann. Diese Änderungen am Browser DOM werden lokal innerhalb der Komponente ausgeführt. So ist es nun möglich, nur einen Teilbaum des Incremental DOM in den Browser DOM zu überführen.
Änderungen am Debugging
Im Development-Modus stellt Angular eine globale Variable ng bereit. Ohne den Einsatz von Ivy war es möglich, mittels ng.probe auf der Konsole mit Komponenten usw. zu interagieren.
ng.probe(elementRef) // debugging context
ng.probe(elementRef).componentInstance // Komponenten-Instanz
ng.probe($0).injector.get(ng.coreTokens.ApplicationRef).tick() // Change detection
Mit Ivy wird diese API geändert und vereinfacht.
ng.getComponent(elementRef) // Komponenten-Instanz
ng.markDirty(componentRef) // Change detection
component.__ngContext__ // Kontext der Komponente
Zusätzlich wird es möglich werden, mittels Breakpoints im Template in Kombination mit den Sourcemaps direkter zu debuggen.
Typsicheres Template
Mit Ivy und Angular 9 sind die Möglichkeiten der Typprüfung stark verbessert worden. Bis zur Version 8 war die Prüfung eingeschränkt.
<tcc-todo-item [todo]="user.todos.next"></tcc-todo-item>
Der Template Compiler stellte bisher nur sicher, dass es an dem Objekt user auch die Eigenschaft todos gibt, die wiederum eine Eigenschaft next besitzen muss. Ob next aber zu dem Input todo zuweisungskompatibel ist, wird nicht geprüft. Zusätzlich werden weder Typen in ngIf, ngFor oder auch ng-template überprüft. Auch die Benutzung von Template-Variablen via #variablieName unterlag nicht der Typ-Prüfung.
Mit Angular 9 kann die Typ-Prüfung mittels zweier Flags schärfer geschaltet werden. Mit dem Flag fullTemplateTypeCheck werden die Typ-Prüfungen in den Embedded Views eingeschaltet. Somit werden Typen vom Compiler in ngIf oder auch ngFor geprüft. Zusätzlich werden auch die Typen geprüft, die Pipes zurückgeben.
Es ist auch möglich, den Compiler die Typen noch viel strenger prüfen zu lassen. Dazu wird das Flag strictTemplates genutzt. Es aktiviert alle Prüfungen des fullTemplateTypeCheck und sorgt darüber hinaus dafür, dass zum Beispiel die Zuweisungskompatibilität der Inputs geprüft wird. Dem interessierten Leser empfehle ich den Guide in der Angular-Dokumentation zu studieren [1].
Ausblick in die Zukunft
Durch die Einführung von Ivy ergeben sich viele spannende Möglichkeiten. Zum einen kann durch die Locality vorkompilierter Code in NPM-Paketen ausgeliefert werden. Aus dem selben Grund ist, unter Zuhilfenahme von dynamic imports, auch das partielle Deployment möglich. Zusätzlich wird auch Lazy-Loading ohne die Kopplung an Routen verfügbar. Für hochdynamische Anwendungen wird es einfach möglich sein, mittels des JiT Compilers Komponenten programmatisch in eine bestehende Angular-Anwendung zu integrieren.
Angular Elements
Mittels Angular Elements lassen sich Angular-Komponenten als Web-Components am Browser registrieren. Dies geht schon seit der Version 6. Durch Ivy und die damit verbundene bessere Fähigkeit zum Treeshaking wird die Bundlesize von Angular Elements stark sinken. Damit wird es noch lohnenswerter, Angular-Komponenten in Form von Web-Components zu verwenden.
Bazel
Mit Angular 9 wird es einen Opt-In für Bazel als Buildsystem geben. Bazel ist ein Build-Tool von Google. Das Ziel von Bazel ist es, inkrementelle, schnelle und reproduzierbare Builds zur Verfügung zu stellen.
- inkrementell – Alle Build-Artefakte werden in einem Cache gesammelt. Die Regeln nach denen gebaut wird – die so genannten Rules – ermöglichen Basel, sehr genau zu bestimmen, welche Teile der Anwendung neu gebaut werden müssen.
- schnell – Wegen des inkrementellen Builds folgt, dass nur geänderte Teile der Anwendung neu gebaut werden müssen. Anders formuliert hängt die Zeit für einen Build nicht mehr von der Größe des Projekts ab, sondern von der Größe der Änderungen am Projekt.
- reproduzierbar – Es werden alle Buildartefakte im Cache vorgehalten, auch die von den Zwischenschritten. In den Build Files müssen alle Abhängigkeiten und alle Schritte die zum Build notwendig sind erfasst sein.
Dabei können die Eigenschaften von Bazel auch sehr gut zur Skalierung genutzt werden. Die Caches, in denen die Artefakte abgelegt werden, lassen sich verteilen. Zusätzlich lässt sich eine Remote Build Farm einrichten und jeder einzelne Schritt lässt sich auch auf diese verschieben. Beispielsweise kann für ein großes Projekt ein Remote-Build über Nacht durchgeführt werden und der Remote-Cache kann dazu genutzt werden, auf den Rechnern einzelner Entwickler auch nur die eigenen Änderungen bauen zu müssen. Zusätzlich wird in Verbindung mit Docker auch ein inkrementelles Deployment möglich. Die Änderung an den Quellen führt zu einem neuen Build-Artefakt. Dieses wiederum erzeugt einen neuen Docker-Layer, der zum einen auch im Cache landet, zum anderen aber auch in die Docker-Registry geladen wird. Mit diesem aktualisierten Layer werden dann die entsprechenden Container neu deployed.
An der Integration von Bazel in das Angular CLI wird noch aktiv entwickelt. Es wird nicht das ganze CLI ausgetauscht werden, sondern nur der Teil des CLI, der für die Orchestrierung des eigentlichen Builds zuständig ist.