If you are rendering your SPA using Angular Universal you may have noticed a common issue with the page loading: the content "blinks" and loads again. This behavior may occur due to duplicate requests being made, once in the server, and then in the client. Angular Universal provides a module called TransferHttpCacheModule
to tackle this problem by caching requests made while rendering the page on the server-side.
Prerequisites
Before starting, add Angular Universal to an existing Angular project by running the following schematic.
ng add @nguniversal/express-engine
I won't delve further into this topic. For an Angular Universal's beginners guide please refer to Server-side rendering (SSR) with Angular Universal.
Setting up caching
Setting up server-side caching is pretty easy. All you have to do is add TransferHttpCacheModule
to your app.module.ts
, and ServerTransferStateModule
to your app.server.module.ts
.
// app.module.ts
import { TransferHttpCacheModule } from '@nguniversal/common';
@NgModule({
imports: [
(...)
TransferHttpCacheModule
]
})
export class AppModule {}
// app.server.module.ts
import { ServerTransferStateModule } from '@angular/platform-server';
@NgModule({
imports: [
(...)
ServerTransferStateModule
]
})
export class AppServerModule {}
After adding these, you will see that the requests done in the server while the page is being rendered will not be duplicated in the browser.
How it works
TransferHttpCacheModule
provides a HTTP interceptor that uses the TransferState
from @angular/platform-browser
. TransferState
is "a key value store that is transferred from the application on the server side to the application on the client side", according to Angular's documentation.
First, when intercepting a request, it checks the cache (key value store) to see if that specific request has been made. This may branch into two different behaviors whether the request has been cached or not.
If the request key is found in the key value store, it will handle the request by returning the cached response.
if (this.transferState.hasKey(storeKey)) {
// Request found in cache. Respond using it.
const response = this.transferState.get(storeKey, {} as TransferHttpResponse);
return observableOf(new HttpResponse<any>({
body: response.body,
headers: new HttpHeaders(response.headers),
status: response.status,
statusText: response.statusText,
url: response.url,
}));
}
If the request key is not found in the cache, it proceeds with the original request and sets a key-value pair to the TransferState
cache with the response.
else {
// Request not found in cache. Make the request and cache it.
const httpEvent = next.handle(req);
return httpEvent
.pipe(
tap((event: HttpEvent<unknown>) => {
if (event instanceof HttpResponse) {
this.transferState.set(storeKey, {
body: event.body,
headers: getHeadersMap(event.headers),
status: event.status,
statusText: event.statusText,
url: event.url || '',
});
}
})
);
}
Handling relative path requests
You may run into issues when using relative paths in requests on the server-side. You can use a HTTP interceptor in this case to transform your relative paths to absolute paths.
import {
HttpInterceptor,
HttpRequest,
HttpHandler,
HttpEvent
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { Inject, Injectable } from '@angular/core';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { Request } from 'express';
function isAbsoluteUrl(url: string) {
return url.startsWith('http') || url.startsWith('//');
}
@Injectable()
export class ServerStateInterceptor implements HttpInterceptor {
constructor(@Inject(REQUEST) private request: Request) {}
intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
if (this.request && !isAbsoluteUrl(req.url)) {
const protocolHost = `${this.request.protocol}://${this.request.get(
'host'
)}`;
const pathSeparator = !req.url.startsWith('/') ? '/' : '';
const url = protocolHost + pathSeparator + req.url;
const serverRequest = req.clone({ url });
return next.handle(serverRequest);
}
return next.handle(req);
}
}
This interceptor works explicitly with the Express engine, so if you are using another engine you will have to adapt it.
Final thoughts
Angular provides out-of-the-box features to easily tackle the duplicate requests issue, with none to minimal configuration from the developer.
I found some other issues working with Angular Universal and third-party packages, so I will be writing about these in the near future.
Sources: