-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Description
Implementation Highlights
- Injected custom storage for OAuthService SSR compatibility
- Replaced direct usage of
window
,location
andlocalStorage
with platform-safe abstractions for SSR compatibility - Enable compatibility with both CSR (Client-Side Rendering) and SSR (Server-Side Rendering).
- Wrapped browser-only and server-only logics in
isPlatformBrowser()
/isPlatformServer()
checks to ensure platform safety during SSR. - Used Angular's
TransferState
to prevent redundant HTTP requests during hydration - Ensured a seamless user experience by avoiding visible content shifts (FOUC) 🟡 (In Progress)
Usage / Examples
1) SSR-Compatible Token Injection (Express)
Below is how you can inject the token from cookies into a custom OAuthStorage during SSR
- Get token from cookie and provide inject token to server side providers
server.use((req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;
commonEngine
.render({
bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: distFolder,
providers: [
{ provide: APP_BASE_HREF, useValue: baseUrl },
{ provide: 'cookies', useValue: JSON.stringify(req.headers.cookie) },
],
})
.then(html => res.send(html))
.catch(err => next(err));
});
- Get injected authentication information from injected token that name is
cookies
in our example then set these information to custom SSR OAuthStorage
import { Inject, Injectable } from '@angular/core';
import { OAuthStorage } from 'angular-oauth2-oidc';
@Injectable({
providedIn: null,
})
export class ServerTokenStorageService implements OAuthStorage {
private cookies: Map<string, string> = new Map();
constructor(@Inject('cookies') c: string | undefined) {
if (c) {
const cookieItems = c.split(';');
for (const item of cookieItems) {
const index = item.indexOf('=');
if (index > -1) {
const key = item.slice(0, index).trim();
const value = item.slice(index + 1).trim();
this.cookies.set(key, value);
}
}
}
}
getItem(key: string): string {
if (this.cookies) {
return this.cookies.get(key);
}
return '';
}
removeItem(key: string): void {}
setItem(key: string, data: string): void {}
}
2) Http Data Caching with TransferState
In Angular applications utilizing Server-Side Rendering (SSR), a common performance challenge is the duplication of HTTP calls. Data is often fetched once on the server (during HTML generation) and then again on the client (when the Angular application bootstraps in the browser). This redundant fetching can negatively impact performance
Our transferStateInterceptor
is designed to solve this very problem.
The transferStateInterceptor acts as an HTTP Interceptor, automatically capturing all GET
requests made via HttpClient
:
On the Server: During the SSR process, the transferStateInterceptor intercepts the HTTP responses from your APIs. It then uses Angular's built-in TransferState mechanism to embed this data directly into the server-rendered HTML, typically within a <script> tag in JSON format.
On the Client: When the Angular application starts in the browser, the transferStateInterceptor checks TransferState for any cached data corresponding to the outgoing HTTP request. If it finds the data, it uses this pre-fetched, cached version instead of making a new network request to the API.
This process ensures your data is fetched once on the server and then "transferred" to the browser. This eliminates unnecessary redundant requests, significantly reduces network traffic, and improves initial page load times, leading to a faster and smoother user experience.
To integrate the transferStateInterceptor into your Angular application, you just need to add it to your provideHttpClient providers using the withInterceptors feature:
// app.config.ts or your relevant provide* function
import { provideHttpClient, withInterceptors, withInterceptorsFromDi } from '@angular/common/http';
import { provideClientHydration } from '@angular/platform-browser';
import { transferStateInterceptor } from './interceptors/transfer-state.interceptor'; // Path to your interceptor
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
// Add our functional interceptor here
withInterceptors([transferStateInterceptor]),
// ... other HttpClient configurations (e.g., withFetch(), withXsrfConfiguration)
),
// `provideClientHydration()` is required for TransferState to work
provideClientHydration(),
// ... other providers
]
};
transferStateInterceptor
export const transferStateInterceptor: HttpInterceptorFn = (
req: HttpRequest<any>,
next: HttpHandlerFn,
): Observable<HttpEvent<any>> => {
const transferState = inject(TransferState);
const platformId = inject(PLATFORM_ID);
if (req.method !== 'GET') {
return next(req);
}
const stateKey = makeStateKey<HttpResponse<any>>(req.urlWithParams);
if (isPlatformBrowser(platformId)) {
const storedResponse = transferState.get<HttpResponse<any>>(stateKey, null);
if (storedResponse) {
transferState.remove(stateKey);
return of(new HttpResponse<any>({ body: storedResponse, status: 200 }));
}
}
return next(req).pipe(
tap(event => {
if (isPlatformServer(platformId) && event instanceof HttpResponse) {
transferState.set(stateKey, event.body);
}
}),
);
};
How To Test
- Navigate
npm > ng-packs
directory and runyarn run dev:ssr
commad. This will generate both client and server bundles and start the development node.js server. Once the process is complete, openlocalhost:4200
in your browser