Seit seinen ersten Tagen nutzt Angular die Bibliothek Zone.js, um herauszufinden, wann die Change Detection einzelne Komponenten auf Änderungen prüfen muss. Die Idee dahinter ist einfach: In einer JavaScript-Anwendung können nur Event Handler gebundene Daten ändern. Also gilt es, herauszufinden, wann ein Event Handler ausgeführt wurde. Dazu klinkt sich zone.js in alle Browserobjekte ein: HTMLInputElement, Promise und XmlHttpRequest sind nur ein paar Beispiele. Diese Vorgehensweise, die die dynamische Natur von JavaScript nutzt, um bestehende Objekte im Nachgang zu ändern, nennt sich „Monkey Patching“.
ZUM NEWSLETTER
Regelmäßig News zur Konferenz und der .NET-Community
Zoneless
Auch wenn der Ansatz im Regelfall gut funktioniert, führt er doch immer wieder zu Problemen. Beispielsweise lassen sich Bugs in diesem Bereich nur schwer diagnostizieren, und da nicht jeder Event Handler zwingend gebundene Daten ändert, läuft die Change Detection mitunter zu häufig. Entwickelt man wiederverwendbare Web Components, die Implementierungsdetails wie die Nutzung von Angular verstecken, muss der Konsument diese trotzdem mit einer bestimmten zone.js-Version einsetzen.
Ab Version 18 unterstützt das Framework nun auch einen Datenbindungsmodus, der ohne zone.js auskommt. Er ist vorerst experimentell, damit das Angular-Team zur neuen Vorgehensweise Feedback sammeln kann. Um diesen Modus zu aktivieren, ist die Funktion provideExperimentalZonelessChangeDetection beim Bootstrapping der Anwendung zu nutzen:
export const appConfig: ApplicationConfig = { providers: [ provideExperimentalZonelessChangeDetection(), […] ] }
Da zone.js die Change Detection nicht mehr triggert, braucht Angular andere Auslöser. Dabei handelt es sich um jene, die auch im Datenbindungsmodus OnPush zum Einsatz kommen:
- Ein mit der async Pipe gebundenes Observable veröffentlicht einen neuen Wert.
- Ein gebundenes Signal veröffentlicht einen neuen Wert.
- Die Objektreferenz eines Inputs ändert sich.
- Ein UI Event mit gebundenem Event Handler tritt auf (z. B. click).
- Die Anwendung triggert die Change Detection manuell.
Das bedeutet, dass jene, die in der Vergangenheit bereits konsequent auf OnPush gesetzt haben, nun relativ problemlos auf Zoneless wechseln können. In Fällen, in denen das nicht einfach möglich ist, kann die Anwendung ohne Bedenken zone.js-basiert bleiben. Das Angular-Team geht davon aus, dass nicht jede bestehende Anwendung auf Zoneless umgestellt wird und unterstützt deswegen zone.js weiterhin.
Für neue Anwendungen bietet sich jedoch das Arbeiten ohne zone.js an, sobald dieser neue Modus nicht mehr experimentell ist. Die geplanten Signal Components werden künftig Zoneless-Anwendungen einfacher machen, da sie durch den konsequenten Einsatz von Signals die Voraussetzungen dafür automatisch erfüllen.
Nach einer Umstellung auf Zoneless lässt sich auch der Verweis auf zone.js aus der angular.json entfernen. Durch den Wegfall dieser Bibliothek werden die Bundles im Production Mode um ca. 11 KB kleiner.
Coalescing Zone
Für neue Anwendungen nutzt das Angular CLI nach wie vor zone.js. Neu ist, dass es nun Code generiert, der standardmäßig Event Coalescing aktiviert:
export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), […] ] };
Event Coalescing bedeutet, dass zone.js für unmittelbar aufeinanderfolgende Events nur ein einziges Mal tätig wird. Als Beispiel nennt das Angular-Team mehrere Click Handler, die aufgrund von Event Bubbling hintereinander angestoßen werden.
Neuerungen bei Router Redirects
Möchte ein Guard eine Umleitung auf eine andere Route veranlassen, liefert er den UrlTree dieser Route zurück. Im Gegensatz zur Methode navigate erlaubt dieser Ansatz jedoch nicht die Angabe von Optionen zur detaillierten Steuerung des Routerverhaltens. Beispielsweise kann die Anwendung nicht festlegen, ob der aktuelle Eintrag in der Browser-History zu überschreiben ist oder ob Parameter, die nicht in dem URL aufscheinen sollen, zu übergeben sind.
Um diese Möglichkeit zu nutzen, kann ein Guard nun auch ein RedirectCommand zurückliefern. Dieses RedirectCommand nimmt neben dem UrlTree ein Objekt vom Typ NavigationBehaviorOptions entgegen, welches das gewünschte Verhalten steuert (Listing 1).
Listing 1
export function isAuth(destination: ActivatedRouteSnapshot) { const router = inject(Router); const auth = inject(AuthService); if (auth.isAuth()) { return true; } const afterLoginRedirect = destination.url.join('/'); const urlTree = router.parseUrl('/login'); return new RedirectCommand(urlTree, { skipLocationChange: true, state: { needsLogin: true, afterLoginRedirect: afterLoginRedirect } as RedirectToLoginState, }); } export const routes: Routes = [ { path: '', redirectTo: 'products', pathMatch: 'full' }, { path: 'products', component: ProductListComponent, }, { path: 'login', component: LoginComponent, }, { path: 'products/:id' component: ProductDetailComponent, canActivate: [isAuth] }, { path: 'error', component: ErrorComponent } ];
Die Eigenschaft skipLocationChange gibt an, dass der Routenwechsel nicht in der Browser-History aufscheinen soll, und beim angegeben State handelt es sich um Werte, die an die zu aktivierende Komponente zu übergeben sind, jedoch nicht in dem URL aufscheinen sollen. Weitere Eigenschaften, die NavigationBehaviorOptions bietet, finden sich unter [2].
Der Vollständigkeit halber zeigt Listing 2 die adressierte Komponente, die die übergebenen Daten ausliest.
Listing 2 @Component({ … }) export class LoginComponent { router = inject(Router); auth = inject(AuthService); state: RedirectToLoginState | undefined; constructor() { const nav = this.router.getCurrentNavigation(); if (nav?.extras.state) { this.state = nav?.extras.state as RedirectToLoginState; } } logout() { this.auth.logout(); } login() { this.auth.login('John'); if (this.state?.afterLoginRedirect) { this.router.navigateByUrl(this.state?.afterLoginRedirect); } } }
Der Einsatz eines RedirectCommands wird nun auch durch das optionale Feature withNavigationErrorHandler unterstützt. Dieses Feature, das das Festlegen eines Handlers zur Behandlung von Routerfehlern erlaubt, kann nun damit das Verhalten des Routers steuern (Listing 3).
Listing 3 export function handleNavError(error: NavigationError) { console.log('error', error); const router = inject(Router); const urlTree = router.parseUrl('/error') return new RedirectCommand(urlTree, { state: { error } }) } export const appConfig: ApplicationConfig = { providers: [ […] provideRouter( routes, withComponentInputBinding(), withViewTransitions(), withNavigationErrorHandler(handleNavError), ), ] };
Eine weitere Neuerung in Sachen Redirects betrifft die Eigenschaft redirectTo in der Routerkonfiguration. Bis jetzt konnte man auf den Namen einer anderen Route verweisen. Nun nimmt diese Eigenschaft auch eine Funktion auf, die sich programmatisch um die Weiterleitung kümmert (Listing 4).
Listing 4 export const routes: Routes = [ { path: '', redirectTo: () => { const router = inject(Router); // return 'products' // Alternative return router.parseUrl('/products'); }, pathMatch: 'full' }, […], ];
Der Rückgabewert dieser Funktion ist entweder ein UrlTree oder ein String mit dem Pfad der gewünschten Route.
Standardinhalte für Content Projection
Das Element ng-content, das Angular als Ziel für die Content Projection nutzt, kann nun einen Standardinhalt aufweisen. Diesen Inhalt zeigt Angular an, wenn der Aufrufer keinen anderen Inhalt übergibt:
<div class="pl-10 mb-20"> <ng-content> <b>Book today to get 5% discount!</b> </ng-content> </div>
Standardinhalte für Content Projection
Das AbstractControl, das u. a. als Basisklasse für FormControl und FormGroup fungiert, weist nun eine Eigenschaft events auf. Dieses Observable informiert über zahlreiche Zustandsänderungen (Listing 5).
Listing 5 export class ProductDetailComponent implements OnChanges { […] formControl = new FormControl<number>(1); […] constructor() { this.formControl.events.subscribe(e => { console.log('e', e); }); } […] }
Die einzelnen Events veröffentlicht es als Objekte vom Typ ControlEvent. Bei ControlEvent handelt es sich um eine abstrakte Basisklasse mit den folgenden Ausprägungen:
- FormResetEvent
- FormSubmittedEvent
- PristineChangeEvent
- StatusChangeEvent
- TouchedChangeEvent
- ValueChangeEvent
Event Replay für SSR
Durch den Einsatz von Server-side Rendering bekommen Aufrufer:innen rascher die angeforderte Seite angezeigt. Danach beginnt der Browser mit dem Laden der JavaScript Bundles, die die Seite erst interaktiv machen. Dieser Vorgang wird auch Hydration genannt. Abbildung 1 veranschaulicht das: FMP steht für „First Meaningful Paint“ und TTI für „Time to Interactive“.
Abb. 1: Uncanny Valley bei SSR
Besondere Beachtung erfordert die Zeitspanne zwischen FMP und TTI, auch bekannt als „Uncanny Valley“. Benutzer:innen bekommen hier die Seite bereits angezeigt, das JavaScript, das auf Benutzerinteraktionen reagiert, ist jedoch noch nicht geladen. Klicks und andere Interaktionen laufen also ins Leere.
Um das zu verhindern, bietet Angular nun Event Replay, das die Interaktionen im Uncanny Valley aufzeichnet und danach wiedergibt. Möglich wird das durch ein minimales Skript, das der Browser initial gemeinsam mit der vorgerenderten Seite lädt.
Event Replay kommt als optionales Feature für provideClientHydration und wird mit der Funktion withEventReplay eingebunden (Listing 6).
Listing 6 export const appConfig: ApplicationConfig = { providers: [ […], provideClientHydration( withEventReplay() ) ] };
Die Implementierung von Event Replay hat sich bei Google schon länger bewährt. Sie stammt aus dem Google-internen Framework Wiz [3], das für seine Fähigkeiten im Bereich SSR und Hydration bekannt ist und deswegen für performancekritische öffentliche Lösungen zum Einsatz kommt.
Auf der diesjährigen ng-conf hat das Angular-Team bekannt gegeben, dass künftig die Angular- und Wiz-Teams verstärkt zusammenarbeiten werden. In einem ersten Schritt nutzt das Wiz-Team die aus Angular stammenden Signals während Angular die erprobten Möglichkeiten zu Event Replay vom Wiz-Team übernommen hat.
Automatischer TransferState für HTTP-Anfragen
Beim Einsatz von SSR cacht der HttpClient schon länger die Ergebnisse serverseitig durchgeführter HTTP-Anfragen, sodass die Anfragen im Browser nicht nochmal ausgeführt werden müssen. Dazu kommt ein Interceptor zum Einsatz, der sich auf das TransferState API stützt. Dieses API bettet die gecachten Daten im Markup der vorgerenderten Seite ein und im Browser greift u. a. der HttpClient darauf zu.
Nun berücksichtigt diese Implementierung auch, dass serverseitig andere URLs als im Browser zum Einsatz kommen können. Dazu lässt sich ein Objekt konfigurieren, das interne, serverseitige URLs auf die URLs für den Einsatz im Browser abbildet. Listing 7 veranschaulicht die Konfiguration dieses Objekts mit einem Beispiel, das aus dem Change-Log [4] übernommen wurde.
Listing 7 // in app.server.config.ts { provide: HTTP_TRANSFER_CACHE_ORIGIN_MAP, useValue: { 'http://internal-domain:80': 'https://external-domain:443' } } // Alternative usage with dynamic values // (depending on stage or prod environments) { provide: HTTP_TRANSFER_CACHE_ORIGIN_MAP, useFactory: () => { const config = inject(ConfigService); return { [config.internalOrigin]: [config.externalOrigin], }; } }
Außerdem platziert der HttpClient nun die Resultate serverseitiger HTTP-Anfragen, die einen Authorization- oder Proxy-Authorization-Header enthalten, nicht mehr automatisch im TransferState. Wer solche Resultate auch künftig cachen möchte, nutzt die neue Eigenschaft includeRequestsWithAuthHeaders:
withHttpTransferCache({ includeRequestsWithAuthHeaders: true, })
DevTools und Hydration
Die Angular DevTools zeigen nun auf Wunsch an, welche Komponenten bereits hydriert wurden. Dazu ist rechts unten die Option Show hydration overlays zu aktivieren (Abb. 2).
Abb. 2: Die DevTools zeigen nun an, welche Komponenten bereits hydriert wurden
Diese Option versieht alle hydrierten Komponenten mit einem blau-transparenten Overlay und zeigt rechts oben zusätzlich ein Wassertropfenicon an.
Migration auf den neuen ApplicationBuilder
Der neue ApplicationBuilder wurde bereits mit Angular 17 eingeführt und automatisch für neue Angular-Anwendungen eingerichtet. Da er auf modernen Technologien wie esbuild basiert, ist er um einiges schneller als der ursprüngliche, webpack-basierte Builder, den Angular nach wie vor unterstützt. Bei ersten Tests konnte ich eine Beschleunigung um den Faktor 3 bis 4 feststellen. Außerdem kommt der ApplicationBuilder mit einer komfortablen Unterstützung für SSR.
Beim Update auf Angular 18 schlägt das CLI nun vor, auch bestehende Anwendungen auf den ApplicationBuilder umzustellen (Abb. 3).
Abb. 3: Das Update auf Version 18 bietet die Migration auf den neuen ApplicationBuilder an
Da das CLI-Team viel Aufwand in Featureparität zwischen der klassischen und der neuen Implementierung investiert hat, sollte diese Umstellung in den meisten Fällen gut funktionieren und die Build-Zeiten drastisch beschleunigen. Auf jeden Fall sollten aber nach der Umstellung die Anwendung sowie der Build auf Funktionstüchtigkeit geprüft werden.
Weitere Neuerungen
Neben den bereits beschriebenen Neuerungen gibt es noch einige kleinere Updates und Verbesserungen:
- @defer funktioniert auch in npm Packages.
- Ein neues Token HOST_TAG_NAME zeigt auf den Tagnamen der aktuellen Komponente.
- Angular I18N spielt nun mit Hydration zusammen.
- Die Module HttpClientModule, HttpClientXsrfModule und HttpClientJsonpModule, sowie das HttpClientTestingModule sind nun deprecated. Als Ersatz kommen die entsprechenden Standalone APIs wie provideHttpClient zum Einsatz. Ein Schematic kümmert sich automatisch um diese Umstellung beim Update auf Angular 18.
- Für neue Projekte richtet nun das CLI einen public-Ordner anstatt eines assets-Ordners ein. Damit möchte sich das Angular-Team an eine allgemeine Gepflogenheit in der Welt der Webentwicklung annähern.
- Für Zoneless-Anwendungen wandelt das CLI async und await nicht mehr in Promises um. Beim Einsatz von zone.js ist das notwendig, da sich Promises monkey-patchen lassen.
- Der ApplicationBuilder cacht nun Zwischenergebnisse. Damit lassen sich die folgenden Builds drastisch beschleunigen.
ZUM NEWSLETTER
Regelmäßig News zur Konferenz und der .NET-Community
Zusammenfassung
Nach mehreren Releases mit vielen neuen Features steht bei Angular 18 vor allem das Abrunden von Ecken im Vordergrund. Es gibt neue Möglichkeiten für Router Redirects, Standardwerte für Content Projection, Event Replay und Verbesserungen bei der Nutzung des TransferState für HTTP-Anfragen. Daneben wurden zahlreiche Bugs behandelt und die Performance des neuen ApplicationBuilder durch Caching verbessert. Außerdem gibt es einen Ausblick auf Zoneless Angular.
Links & Literatur
[1] https://github.com/manfredsteyer/hero-shop.git
[2] https://angular.dev/api/router/NavigationBehaviorOptions
[3] https://blog.angular.dev/angular-and-wiz-are-better-together-91e633d8cd5a