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 17. - 20. März 2025 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.
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
Bei neuen Artikel & Eventupdates informiert werden:
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.