HTTP_INTERCEPTOR
with a TransferState service, to prevent duplicate calls to server resourcesng new angular-universal-transfer-state --style css --routing true --directory angularApp
cd angular-universal-transfer-state
ng serve
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ ** Date: 2018-10-29T08:58:37.685Z Hash: cb54e4608cfb1115882b Time: 7682ms chunk {main} main.js, main.js.map (main) 10.7 kB [initial] [rendered] chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 227 kB [initial] [rendered] chunk {runtime} runtime.js, runtime.js.map (runtime) 5.22 kB [entry] [rendered] chunk {styles} styles.js, styles.js.map (styles) 15.9 kB [initial] [rendered] chunk {vendor} vendor.js, vendor.js.map (vendor) 3.29 MB [initial] [rendered]
ng add @ng-toolkit/universal
npm run build:prod;npm run server
curl http://localhost:8080
...
”) in the code below indicates a section redacted for brevity.<!DOCTYPE html><html lang="en"><head> <meta charset="utf-8"> <title>angular-universal-i18n</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="stylesheet" href="styles.3bb2a9d4949b7dc120a9.css"><style ng-transition="app-root"> </style></head> <body> <app-root _nghost-sc0="" ng-version="7.0.4"><div _ngcontent-sc0="" style="text-align:center"> <h1 _ngcontent-sc0=""> Welcome to angular-universal-i18n! </h1> ... </div><h2 _ngcontent-sc0="">Here are some links to help you start: </h2> <ul _ngcontent-sc0=""> <li _ngcontent-sc0=""><h2 _ngcontent-sc0=""> <a _ngcontent-sc0="" href="https://angular.io/tutorial" rel="noopener" target="_blank">Tour of Heroes</a></h2> </li> <li _ngcontent-sc0=""><h2 _ngcontent-sc0=""> <a _ngcontent-sc0="" href="https://github.com/angular/angular-cli/wiki" rel="noopener" target="_blank">CLI Documentation</a></h2> </li> <li _ngcontent-sc0=""><h2 _ngcontent-sc0=""> <a _ngcontent-sc0="" href="https://blog.angular.io/" rel="noopener" target="_blank">Angular blog</a></h2> </li></ul></app-root> <script type="text/javascript" src="runtime.ec2944dd8b20ec099bf3.js"></script> <script type="text/javascript" src="polyfills.c6871e56cb80756a5498.js"></script> <script type="text/javascript" src="main.f27bf40180c4a8476e2e.js"></script> <script id="app-root-state" type="application/json">{}</script></body></html>
git clone https://github.com/maciejtreder/angular-universal-transfer-state.git cd angular-universal-transfer-state git checkout step1 cd angularApp npm install npm run build:prod npm run server
externalApi
directory outside of the angular-universal-transfer-state
application’s directory structure. In that directory create a file externalApi.js
(so the relative path would be: ../externalApi/externalApi.js
) and place the following code in it:const express = require('express'); const app = express(); app.get('/api/fast', (req, res) => { console.log('fast endpoint hit'); res.send({response: 'fast'}); }); app.get('/api/slow', (req, res) => { setTimeout(() => { console.log('slow endpoint hit'); res.send({response: 'slow'}); }, 5000); }); app.listen(8081, () => { console.log('Listening'); });
package.json
file in the externalApi
directory and place following content inside:{ "name": "angular-universal-transfer-state", "version": "0.0.0", "scripts": { "start": "node externalApi.js" }, "private": true, "dependencies": { "express": "^4.16.4" }, "devDependencies": {} }
externalApi
directory:npm install
externalApi
we just created. Generate it by typing following command in the console in the angular-universal-transfer-state/angularApp
directory:ng g s custom --spec false
CustomService
implementation inside src/app/custom.service.ts
:import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class CustomService { constructor(private http: HttpClient) {} public getFast(): Observable<any> { return this.http.get<any>('http://localhost:8081/api/fast'); } public getSlow(): Observable<any> { return this.http.get<any>('http://localhost:8081/api/slow'); } }
HttpClientModule
in our application because we are injecting a HttpClient
service into CustomService
. Replace content of the src/app/app.module.ts
file with the following code:import { NgtUniversalModule } from '@ng-toolkit/universal'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { FastComponent } from './fast/fast.component'; import { SlowComponent } from './slow/slow.component'; import { HttpClientModule } from '@angular/common/http'; @NgModule({ declarations: [ AppComponent, FastComponent, SlowComponent ], imports: [ CommonModule, NgtUniversalModule, AppRoutingModule, HttpClientModule ] }) export class AppModule { }
fast
endpoint:ng g c fast -m app -s -t --spec false
src/app/fast/fast.component.ts
:import { Component, OnInit } from '@angular/core'; import { CustomService } from '../custom.service'; import { Observable } from 'rxjs'; @Component({ selector: 'app-fast', template: ` <p> Response is: {{response | async | json}} </p> `, styles: [] }) export class FastComponent { public response: Observable<any> = this.service.getFast(); constructor(private service: CustomService) { } }
slow
endpoint:ng g c slow -m app -s -t --spec false
src/app/slow/slow.component.ts
:import { Component } from '@angular/core'; import { CustomService } from '../custom.service'; import { Observable } from 'rxjs'; @Component({ selector: 'app-slow', template: ` <p> Response is: {{response | async | json}} </p> `, styles: [] }) export class SlowComponent { public response: Observable<any> = this.service.getSlow(); constructor(private service: CustomService) {} }
src/app/app-routing.module.ts
file with:import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { FastComponent } from './fast/fast.component'; import { SlowComponent } from './slow/slow.component'; const routes: Routes = [ {path: '', redirectTo: 'fast', pathMatch: 'full'}, {path: 'fast', component: FastComponent}, {path: 'slow', component: SlowComponent} ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
src/app/app.component.html
:<div style="text-align:center"> <h1> Welcome to {{ title }}! </h1> <a routerLink='/fast'>fast</a> <a routerLink='/slow'>slow</a> </div> <router-outlet></router-outlet>
git clone https://github.com/maciejtreder/angular-universal-transfer-state.git cd angular-universal-transfer-state git checkout step2 cd externalApi npm install cd ../angularApp npm install
externalApi.js
in the externalApi
directory:node externalApi.js
angular-universal-transfer-state
directory:npm run build:prod npm run server
externalApi
is running):node externalApi.js Listening fast endpoint hit fast endpoint hit
fast
endpoint twice. How it could that be, when we opened the website only once?externalApi
fast
endpoint while serving Angular to the client,externalApi
fast
endpoint againexternalApi
fast
endpoint response is returned to the browser and is placed in the application view.TransferState
service, a key-value registry exchanged between the Node.js server and the application rendered in the browser. We will use it through an HTTP_INTERCEPTOR
mechanism which will reside inside the HttpClient
service and which will manipulate the requests and responses.ng g s HttpInterceptor --spec false
src/app/http-interceptor.service.ts
with this code:import { Injectable, Inject, PLATFORM_ID } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse } from '@angular/common/http'; import { Observable, of } from 'rxjs'; import { tap } from 'rxjs/operators'; import { TransferState, makeStateKey, StateKey } from '@angular/platform-browser'; import { isPlatformServer } from '@angular/common'; @Injectable({ providedIn: 'root' }) export class HttpInterceptorService implements HttpInterceptor { constructor(private transferState: TransferState, @Inject(PLATFORM_ID) private platformId: any) {} public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { if (request.method !== 'GET') { return next.handle(request); } const key: StateKey<string> = makeStateKey<string>(request.url); if (isPlatformServer(this.platformId)) { return next.handle(request).pipe(tap((event) => { this.transferState.set(key, (<HttpResponse<any>> event).body); })); } else { const storedResponse = this.transferState.get<any>(key, null); if (storedResponse) { const response = new HttpResponse({body: storedResponse, status: 200}); this.transferState.remove(key); return of(response); } else { return next.handle(request); } } } }
HttpInterceptor
interface, so we need to implement a corresponding method:public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>
HttpClient
service.TransferState
registry only for GET calls. We need to check to see if a call matches that criteria:if (request.method !== 'GET') { return next.handle(request); }
const key: StateKey<string> = makeStateKey<string>(request.url);
isPlatformServer
method from the @angular/common
library together with the PLATFORM_ID
injection token:if (isPlatformServer(this.platformId)) { //serverSide } else { //browserSide }
TransferState
registry:if (isPlatformServer(this.platformId)) { return next.handle(request).pipe(tap((event) => { this.transferState.set(key, (<HttpResponse<any>> event).body); }));
CustomService
in this case). If the given key doesn’t exist in the registry we simply perform the HTTP call:else { const storedResponse = this.transferState.get<any>(key, null); if (storedResponse) { const response = new HttpResponse({body: storedResponse, status: 200}); this.transferState.remove(key); return of(response); } else { return next.handle(request); } }
src/app/app.module.ts
by replacing the existing code with the following:import { NgtUniversalModule } from '@ng-toolkit/universal'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { FastComponent } from './fast/fast.component'; import { SlowComponent } from './slow/slow.component'; import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { HttpInterceptorService } from './http-interceptor.service'; @NgModule({ declarations: [ AppComponent, FastComponent, SlowComponent ], imports: [ CommonModule, NgtUniversalModule, AppRoutingModule, HttpClientModule ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: HttpInterceptorService, multi: true } ] }) export class AppModule { }
TransferState
service into our application. Include ServerTransferStateModule
in the server-side module by replacing the existing code in src/app/app.server.module.ts
with the following:import { AppComponent } from './app.component'; import { AppModule } from './app.module'; import {NgModule} from '@angular/core'; import {ServerModule, ServerTransferStateModule} from '@angular/platform-server'; import {ModuleMapLoaderModule} from '@nguniversal/module-map-ngfactory-loader'; import { BrowserModule } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @NgModule({ bootstrap: [AppComponent], imports: [ BrowserModule.withServerTransition({appId: 'app-root'}), AppModule, ServerModule, NoopAnimationsModule, ModuleMapLoaderModule, ServerTransferStateModule ] }) export class AppServerModule {}
BrowserTransferStateModule
in the browser-side module by replacing the code in src/app/app.browser.module.ts
with the following code:import { AppComponent } from './app.component'; import { AppModule } from './app.module'; import { NgModule } from '@angular/core'; import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser'; @NgModule({ bootstrap: [AppComponent], imports: [ BrowserModule.withServerTransition({appId: 'app-root'}), AppModule, BrowserTransferStateModule ] }) export class AppBrowserModule {}
src/main.ts
file also needs to be changed. We need to bootstrap our app in a slightly different way to make the TransferState
registry work properly; we need to bootstrap our app when the DOMContentLoaded
event is emitted by the browser. Replace the existing code with the following:import { AppBrowserModule } from '.././src/app/app.browser.module'; import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } document.addEventListener('DOMContentLoaded', () => { platformBrowserDynamic() .bootstrapModule(AppBrowserModule) .catch(err => console.log(err)); });
npm run build:prod npm run server
externalApi
process for any reason you should restart it now.externalApi
process. It should look like the following, in which the first two fast endpoint responses are from the previous test and the last line is the result of the current test:Listening fast endpoint hit fast endpoint hit slow endpoint hit
TransferState
registry.HTTP_INTERCEPTOR
, you can use the standard TransferHttpCacheModule
from the @nguniversal
library. This makes implementation more convenient, but it also imposes a constraint: you can’t make any changes to the standard library, so you would not be able to add functionality like the API watchdog we are going to create in a forthcoming step.npm install @nguniversal/common
TransferHttpCacheModule
module into src/app/app.module.ts
by replacing the contents with the following code:import { NgtUniversalModule } from '@ng-toolkit/universal'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { FastComponent } from './fast/fast.component'; import { SlowComponent } from './slow/slow.component'; import { HttpClientModule } from '@angular/common/http'; import { TransferHttpCacheModule } from '@nguniversal/common'; @NgModule({ declarations: [ AppComponent, FastComponent, SlowComponent ], imports: [ CommonModule, NgtUniversalModule, AppRoutingModule, HttpClientModule, TransferHttpCacheModule ] }) export class AppModule { }
git clone https://github.com/maciejtreder/angular-universal-transfer-state.git cd angular-universal-transfer-state git checkout step3 cd externalApi npm install cd ../angularApp npm install
RouteResolver
in the call to the SlowComponent
.ng g s SlowComponentResolver --spec false
src/app/slow-component-resolver.service.ts
with the following:import { Injectable, Inject, PLATFORM_ID } from '@angular/core'; import { Resolve } from '@angular/router'; import { Observable, timer } from 'rxjs'; import { isPlatformBrowser } from '@angular/common'; import { CustomService } from './custom.service'; import { takeUntil } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class SlowComponentResolverService implements Resolve<any> { constructor(private service: CustomService, @Inject(PLATFORM_ID) private platformId: any) { } public resolve(): Observable<any> { if (isPlatformBrowser(this.platformId)) { return this.service.getSlow(); } const watchdog: Observable<number> = timer(500); return Observable.create(subject => { this.service.getSlow().pipe(takeUntil(watchdog)).subscribe(response => { subject.next(response); subject.complete(); }); watchdog.subscribe(() => { subject.next(null); subject.complete(); }); }); } }
resolve
method that we need to implement because we are implementing the Resolve
interface. Inside the method we are checking to see if the code is executing on the browser or server. If the code is being executed in the browser it waits for the call as long as necessary by executing the call:if (isPlatformBrowser(this.platformId)) { return this.service.getSlow(); }
watchdog
, is created using the timer
method from the rxjs
library. The timer
method creates an observable which emits a value only once after given amount of time in milliseconds:const watchdog: Observable<number> = timer(500);
takeUntil
method, piped to the request call. If the observable emits a value before the API sends a response it pushes null
to the component. Otherwise it pushes the API response.return Observable.create(subject => { this.service.getSlow().pipe(takeUntil(watchdog)).subscribe(response => { subject.next(response); subject.complete(); }); watchdog.subscribe(() => { subject.next(null); subject.complete(); }); }); }
src/app/app-routing.module.ts
with the following:import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { FastComponent } from './fast/fast.component'; import { SlowComponent } from './slow/slow.component'; import { SlowComponentResolverService } from './slow-component-resolver.service'; const routes: Routes = [ {path: '', redirectTo: 'fast', pathMatch: 'full'}, {path: 'fast', component: FastComponent}, {path: 'slow', component: SlowComponent, resolve: {response: SlowComponentResolverService}} ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
src/app/slow/slow.component.ts
with the following:import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-slow', template: ` <p> Response is: {{response | json}} </p> `, styles: [] }) export class SlowComponent { public response: any = this.router.snapshot.data.response; constructor(private router: ActivatedRoute) {} }
HTTP_INTERCEPTOR
.git clone https://github.com/maciejtreder/angular-universal-transfer-state.git cd angular-universal-transfer-state git checkout step4 npm install npm run build:prod npm run server
TransferState
service we are able to limit calls made to potentially slow APIs. We also implemented a watchdog mechanism to abandon long-running API calls which can adversely impact the total time a server needs to render a view. Both these techniques help improve the performance of Angular websites, which increases user satisfaction and helps the site score better in search engine ranking.