Blog

HttpClient in Angular 15, Standalone APIs und funktionale Interceptors

Dez 20, 2022

Zweifelsfrei gehört der HttpClient zu den bekanntesten Services im Lieferumfang von Angular. Für Version 15 hat ihn das Angular-Team nun an die neuen Standalone Components angepasst. Bei dieser Gelegenheit wurde auch das Interceptors-Konzept überarbeitet. In diesem Artikel gehe ich auf diese Neuerungen ein.

Das dazu verwendete Beispiel findet man unter [1].

Standalone APIs für HttpClient?

Angular 14 brachte die langersehnten Standalone Components, die nun endlich Angular-Module optional machen. Allerdings sind solche eigenständigen Komponenten nur eine Seite der Medaille, denn Komponenten stützen sich in der Regel auf Services. Solche Services gilt es jedoch bereitzustellen, und genau das ist eine der Aufgaben von Angular-Modulen, die wir nun aber loswerden wollen.

Zum Glück gibt es bereits seit einiger Zeit eine Möglichkeit, Services ohne Angular-Module bereitzustellen, nämlich via @Injectable({providedIn: ‚root‘}). Auch wenn diese Vorgehensweise zu bevorzugen ist, lässt sie sich leider nicht immer anwenden. Gerade dann, wenn der Service parametrisierbar sein soll, stößt man an die Grenzen dieser komfortablen Option. Deswegen erlaubt Angular auch beim Bootstrapping der AppComponent, Services bereitzustellen:

bootstrapApplication(AppComponent, {
  providers: [
    MyService,
    importProvidersFrom(HttpClientModule)
  ]
}

Diese Services richtet Angular im Root Scope ein. Somit entsprechen diese Services u. a. jenen, die eine Angular-Anwendung früher im AppModule registriert hat. Die vom Angular-Team angebotene Funktion importProvidersFrom erlaubt den Brückenschlag zu Services in bereits existierenden Angular-Modulen. Somit lässt sich Bestandscode ohne Änderungen nutzen.

Der Einsatz von importProvidersFrom ist jedoch nur eine Übergangslösung, zumal künftig Angular-Anwendungen gänzlich ohne Angular-Module auskommen sollen. Die Lösung für dieses Problem nennt sich Standalone APIs. Sie erlauben das Einrichten von Services ohne Umweg über Module. Mit Version 15 erhält Angular nun Standalone APIs für den HttpClient (Listing 1).

Listing 1

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor]),
    ),
  ]
}

Die neue Funktion provideHttpClient liefert die Services rund um den HttpClient. Außerdem aktiviert sie optionale Features des HttpClient. Für jedes Feature steht eine eigene Funktion zur Verfügung. Die Funktion withInterceptors aktiviert zum Beispiel die Unterstützung für http Interceptors.

Die Kombination aus einer provideXYZ-Funktion und mehreren optionalen withXYZ-Funktionen ist hier nicht willkürlich gewählt, sondern entspricht einem Muster, das das Angular-Team generell für Standalone APIs vorsieht. Anwendungsentwickler:innen müssen somit beim Einrichten einer neuen Bibliothek nach Funktionen Ausschau halten, die mit provide oder with beginnen.

Dieses Muster führt übrigens zu einem sehr angenehmen Nebeneffekt: Bibliotheken werden besser Tree-shakable. Das liegt daran, dass sich über eine statische Quellcodeanalyse sehr einfach herausfinden lässt, ob die Anwendung eine Funktion jemals aufruft. Bei Methoden ist das aufgrund der Möglichkeit einer polymorphen Nutzung der zugrundeliegenden Objekte nicht ganz so einfach.

In guter Gesellschaft

Es ist nicht das erste Mal, dass das Angular-Team für eine bestehende Bibliothek Standalone APIs einführt. Bereits Angular 14.2 kam mit entsprechenden APIs für den Router. Auch hier hält sich das Framework an das besprochene Muster:

provideRouter(APP_ROUTES,
  withPreloading(PreloadAllModules),
),

Etwa zur selben Zeit hat auch das NgRx-Team eine Standalone-API für das Einrichten von Reducern, Effects sowie für die die Nutzung der Redux Dev Tools eingeführt:

provideStore(reducer),
provideEffects([]),
provideStoreDevtools(),

Funktionale Interceptors

Beim Einführen von Standalone APIs hat das Angular-Team auch gleich die Gelegenheit genutzt, den HttpClient ein wenig zu überarbeiten. Ein Ergebnis daraus sind die neuen funktionalen Interceptors. Sie erlauben es, Interceptors als einfache Funktion auszudrücken. Ein eigener Service, der ein vorgegebenes Interface realisiert, ist nicht mehr notwendig (Listing 2).

Listing 2

import { HttpInterceptorFn } from "@angular/common/http";
import { tap } from "rxjs";
 
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  console.log('request', req.method, req.url);
  console.log('authInterceptor')
 
  if (req.url.startsWith('https://demo.angulararchitects.io/api/')) {
    // Setting a dummy token for demonstration
    const headers = req.headers.set('Authorization', 'Bearer Auth-1234567');
    req = req.clone({headers});
  }
 
  return next(req).pipe(
    tap(resp => console.log('response', resp))
  );
}

Der gezeigte Interceptor erweitert HTTP-Aufrufe, die an bestimmte URLs gerichtet sind, um ein beispielhaftes Securitytoken. Abgesehen davon, dass der Interceptor nun eine Funktion des Typs HttpInterceptorFn ist, hat sich an der prinzipiellen Funktionsweise des Konzeptes nichts geändert. Wie in Listing 1 gezeigt, lassen sich funktionale Interceptors mit withInterceptors beim Aufruf von provideHttpClient einrichten.

Auch in guter Gesellschaft

Auch funktionale Interceptors folgen einem Trend, der bereits mit Angular 14.2 begonnen hat. Seit dieser Version lassen sich Guards und Resolver auch als Funktionen darstellen:

{
  path: 'passenger-search',
  component: PassengerSearchComponent,
  canActivate: [
    () => inject(AuthService).isAuthenticated()
  ]
},

Die neue inject-Funktion erlaubt es, solche Funktionen mit Services zu versorgen.

ANGULAR LIVE IN ACTION?

Die Angular-Workshops vom 18. - 21. März 2024 entdecken

Interceptors und Lazy Loading

Seit jeher führen Interceptors in Lazy-Modulen zu Verwirrung: Sobald ein Lazy-Modul eigene Interceptors einführt, werden jene der übergeordneten Scopes – z. B. im Root Scope – nicht mehr angestoßen.

Auch wenn Module mit Standalone Components und APIs der Vergangenheit angehören, bleibt die prinzipielle Problematik bestehen, zumal nun (Lazy-)Routenkonfigurationen eigene Services einrichten können (Listing 3).

Listing 3

export const FLIGHT_BOOKING_ROUTES: Routes = [{
  path: '',
  component: FlightBookingComponent,
  providers: [
    MyService,
    provideState(bookingFeature),
    provideEffects([BookingEffects])
    provideHttpClient(
      withInterceptors([bookingInterceptor]),
      withRequestsMadeViaParent(),
    ),
  ],
}];

Diese Services entsprechen denjenigen Services, die die Anwendung früher in Lazy-Modulen registriert hat. Technisch gesehen führt Angular immer dann, wenn solch ein providers-Array vorliegt, einen eigenen Injector ein. Dieser sogenannte Environment Injector definiert einen Scope für die aktuelle Route sowie ihre Kindrouten.

Wie schon bei den Services beim Bootstrappen der AppComponent gilt auch hier, dass @Injectable({providedIn: ‚root‘}) wenn möglich zu bevorzugen ist. Es ist zwar nicht offensichtlich, aber solche Services, die die Anwendung im Root Scope platziert, funktionieren auch mit Lazy Loading. Kommt der Service nur in einem Lazy-Anwendungsteil zum Einsatz, lädt ihn die Anwendung gemeinsam mit diesem Anwendungsteil. Das hat mit Angular selbst wenig zu tun, sondern mehr mit der Art und Weise, wie Bundlers funktionieren.

Möchte die Anwendung die eingerichteten Services hingegen konfigurieren, muss sie mit dem gezeigten providers-Array vorliebnehmen. Ein gutes Beispiel ist das in Listing 4 gezeigte Einrichten eines Feature-Slice samt Effects für NgRx.

Die Funktion provideHttpClient lässt sich ebenfalls im providers-Array nutzen, um Interceptors für den Lazy-Teil der Anwendung zu registrieren. Standardmäßig gilt dabei die zuvor besprochene Regel: Existieren Interceptors im aktuellen Environment Injector, ignoriert Angular die Interceptors in übergeordneten Scopes.

Genau dieses Verhalten lässt sich jedoch mit withRequestsMadeViaParent ändern. Diese Methode führt dazu, dass Angular nach den Interceptors im eigenen Scope jene in den übergeordneten Scopes anstößt.

Allerdings gibt es hier eine nicht offensichtliche Falle: Ein Service im Root Scope weiß nichts vom HttpClient und den registrierten Interceptors im inneren Scope. Er greift immer auf den HttpClient im Root Scope zu und somit kommen auch nur die dort eingerichteten Interceptors zur Ausführung. Abbildung 1 veranschaulicht das.

steyer_standalone_http_01.tif_fmt1.jpg
Um dieses Problem zu lösen, könnte die Anwendung den äußeren Service auch im providers-Array der Routenkonfiguration und somit im inneren Scope registrieren. Generell scheint es jedoch sehr schwierig zu sein, solche Konstellationen im Überblick zu behalten. Deswegen könnte es sinnvoll sein, auf Interceptors in inneren Scopes gänzlich zu verzichten. Als Alternative bietet sich ein generischer Interceptor im Root Scope an, der ggf. sogar Zusatzlogiken mit einem dynamischen import aus Lazy-Programmteilen lädt.

Legacy Interceptors und weitere Features

Auch wenn die neuen funktionalen Interceptors sehr charmant sind, können Anwendungen nach wie vor die ursprünglichen klassenbasierten Interceptors nutzen. Diese Option lässt sich mit der Funktion withLegacyInterceptors aktivieren. Anschließend sind die klassenbasierten Interceptors wie gewohnt über einen Multiprovider zu registrieren (Listing 4).

Listing 4

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor]),
      withLegacyInterceptors(),
    ),
    {
      provide: HTTP_INTERCEPTORS,
      useClass: LegacyInterceptor,
      multi: true,
    },
  ]
});

Daneben bringt der HttpClient noch weitere Features, die sich ebenfalls mit with-Funktionen aktivieren lassen: withJsonpSupport aktiviert zum Beispiel die Unterstützung für JSONP und withXsrfConfiguration konfiguriert Details zur Nutzung von XSRF-Tokens. Ruft die Anwendung withXsrfConfiguration nicht auf, kommen Standardeinstellungen zum Einsatz. Um die Nutzung von XSRF-Tokens komplett zu deaktivieren, ist hingegen withNoXsrfProtection aufzurufen.

Stay tuned

Immer auf dem Laufenden bleiben! Alle News & Updates:

Zusammenfassung

Der überarbeitete HttpClient harmoniert nun wunderbar mit Standalone Components und den damit einhergehenden Konzepten wie Environment Injectors. Bei der Gelegenheit hat das Angular-Team auch die Interceptors überarbeitet: Sie lassen sich nun in Form von einfachen Funktionen umsetzen und beim Einbinden des HttpClients registrieren. Außerdem besteht nun auch die Möglichkeit, Interceptors in übergeordneten Scopes zu berücksichtigen.

Immer auf dem Laufenden bleiben!
Alle News & Updates: