Eigene Transfer State Übergabe in Angular Universal durchführen

Angular Universal (=Server Side Rendering) bietet die Möglichkeit, Ergebnisse von HTTP-Aufrufen, welche schon am Server stattgefunden haben, zu cachen und im Browser gleich direkt zu verwenden anstatt einen neuerlichen Aufruf durchzuführen.

Haben wir beispielsweise eine Seite welche über eine API eine Firmen-Liste abruft und anzeigt, würde diese API-Abfrage in einer SSR-Umgebung 2 mal laufen: das erste mal am Node.js-Server und das zweite mal beim Client im Browser. Angular kann nun das Ergebnis der API-Abfrage am Server in den erzeugten HTML-Code für den Client „verpacken“ und dann im Browser daraus abgefragt werden.

Einfache Lösung für die meisten Fälle

In vielen Fällen genügt es dafür, das TransferHttpCacheModule zu verwenden. Dieses macht nichts anderes, als einen Interceptor für HTTP-Aufrufe zu installieren, das Ergebnis in einen TransferState zu speichern und in den HTML-Code einzupacken. Am Browser wird bei jedem HTTP-Aufruf auch in einem Interceptor dann nachgesehen, ob dieselbe URL schon im TransferState vorhanden ist und wenn ja, das dort gespeicherte Ergebnis verwendet anstatt den Call nocheinmal auszuführen.

Problem und Lösung mit eigenen TransferState Interceptor

Die einfache Lösung kann in bestimmten Fällen Probleme machen:

  • wenn beim GET-Request in der URL verschiedene Query-Parameter gesetzt werden, welche vielleicht am Server nicht vorhanden sind oder unterschiedlich zum Browser, aber das Ergebnis nicht beeinflussen
  • wenn am Server die URL für die API unterschiedlich ist als im Browser. Beispielsweise kann am Server die API wenn sie am gleichen Server läuft unter localhost erreichbar sein, im Browser aber unter einer bestimmten Domain

Man kann deshalb auch einen eigenen Interceptor installieren, welcher im Prinzip dasselbe macht wie oben erwähntes Modul, aber man hat hier mehr Kontrolle darüber wie die Daten gespeichert werden.

Hier ein einfaches Beispiel für einen solchen Interceptor (Angular >= 16):

export const transferStateInterceptor: HttpInterceptorFn = (req, next) => {

    const _platformId = inject(PLATFORM_ID);
    const transferState = inject(TransferState);
   
    // wir cachen nur GET-Abfragen, alle anderen werden weitergeleitet
    if (req.method !== 'GET') {
        return next(req);
    }

    const url = new URL(req.urlWithParams);
    const urlWithoutHost = url.toString().substring(url.origin.length);
    const key: StateKey<string> = makeStateKey<string>(urlWithoutHost);

    // am Server schreiben wir das Ergebnis des calls in den TransferState
    if (isPlatformServer(_platformId)) {
        return next(req).pipe(tap((event) => {
            transferState.set(key, (<HttpResponse<any>> event).body);
        }));
    }

    // im Client Browser sehen wir nach ob der Call schon im TransferState ist 
    const storedResponse = transferState.get<any>(key, null);
    if (storedResponse) {
        // wenn ja, geben wir das Ergebnis aus dem TransferState zurück
        const response = new HttpResponse({body: storedResponse, status: 200});
        transferState.remove(key);
        return of(response);
    } else {
        // wenn nicht, wird er ganz normal weitergeleitet und ausgeführt
        return next(req);
    }
}Code-Sprache: TypeScript (typescript)

Um die Default-Übergabe von Ergebnissen welche Angular durchführt zu verhinden, kann man diese Ausschalten. Dadurch wird der HTML-Code kleiner, da ansonsten jedes Ergebnis doppelt übergeben würde.

In der Applikation-Konfiguration (app.config.ts) kann das über einen Parameter eingestellt werden:

        
export const appConfig: ApplicationConfig = {
    providers: [
        provideClientHydration(
            withNoHttpTransferCache()
        ),
...Code-Sprache: JavaScript (javascript)