Summer. Sea. JavaScript.
Angular Universal A medicine for the SEO/CDN issues
Maciej Treder
@maciejtreder
Outline
• SPA pitfall
• Server-side rendering
• Server vs. Browser
• API optimization
• Deployment
• Prerendering & Summary
SPA pitfall SSR Server vs. Browser APIs Deploy Summary
SPA pitfall
ng build
• Ahead of Time compilation
Server vs.
—prod flag
• Ahead of Time compilation
• Minified
• Tree-shaked
SPA pitfallServer vs.
ng build vs. —prod
SPA pitfallServer vs.
SPA problem<IfModule mod_rewrite.c> RewriteEngine On RewriteBase / RewriteRule ^index\.html$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . index.html [L]</IfModule>
.htaccess
SPA pitfallServer vs.
SPA Problem
GET /
GET /anotherPage
index.html
GET /s
ubpa
ge
GET /contact
GET /home
SPA pitfallServer vs.
SPA Problem
GET / GET /anotherPage
SPA pitfallServer vs.
SPA Problem
SPA pitfallServer vs.
Server Side Rendering
SSRServer vs.
Server Side Rendering
GET /GET /anotherPage
SSRServer vs.
Is it worth?curl localhost:8080 <!DOCTYPE html><html lang="en"><head> <meta charset="utf-8"> <title>SomeProject</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.3ff695c00d717f2d2a11.css"><style ng-transition="app-root"> /*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsImZpbGUiOiJzcmMvYXBwL2FwcC5jb21wb25lbnQuY3NzIn0= */</style></head> <body>
<script type="text/javascript" src="runtime.26209474bfa8dc87a77c.js"></script><script type="text/javascript" src="es2015-polyfills.c5dd28b362270c767b34.js" nomodule=""></script><script type="text/javascript" src="polyfills.8bbb231b43165d65d357.js"></script><script type="text/javascript" src="main.8a9128130a3a38dd7ee5.js"></script>
<script id="app-root-state" type="application/json">{}</script></body></html>
<app-root _nghost-sc0="" ng-version="7.2.9"><div _ngcontent-sc0="" style="text-align:center"><h1 _ngcontent-sc0=""> Welcome to someProject! </h1><img _ngcontent-sc0="" alt="Angular Logo" src="data:image/
svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==" width="300"></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://angular.io/cli" 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>
SSRServer vs.
SSRServer vs.
—prod vs. universal
SSRServer vs.
—prod vs. universal
Load HTML Bootstrap
Load HTML Bootstrap
SSR
NO SSR First meaningful paint
First meaningful paint
SSRServer vs.
How to start?
Official guide
https://angular.io/guide/universal
ng-toolkit
https://github.com/maciejtreder/ng-toolkit
SSRServer vs.
ng add @nguniversal/express-engine
CREATE src/main.server.ts (220 bytes) CREATE src/app/app.server.module.ts (318 bytes) CREATE src/tsconfig.server.json (219 bytes) CREATE webpack.server.config.js (1360 bytes) CREATE server.ts (1500 bytes) UPDATE package.json (1876 bytes) UPDATE angular.json (4411 bytes) UPDATE src/main.ts (432 bytes) UPDATE src/app/app.module.ts (359 bytes)
SSRServer vs.
Adjust your modulesapp.module.ts app.server.module.ts
@NgModule({ bootstrap: [AppComponent], imports: [ BrowserModule.withServerTransition({appId: 'my-app'}), //other imports ],
})export class AppModule {}
import {NgModule} from '@angular/core';import {ServerModule} from '@angular/platform-server';import {ModuleMapLoaderModule} from ‘@nguniversal/module-map-ngfactory-loader';
import {AppModule} from './app.module';import {AppComponent} from './app.component';
@NgModule({ imports: [ AppModule, ServerModule, ModuleMapLoaderModule ], bootstrap: [AppComponent],})export class AppServerModule {}
SSRServer vs.
Adjust your modules
Official guide
app.module.ts app.server.module.ts @NgModule({ declarations: [AppComponent], imports: [ //common imports ]})export class AppModule {}
import {NgModule} from '@angular/core';import {ServerModule} from '@angular/platform-server';import {ModuleMapLoaderModule} from ‘@nguniversal/module-map-ngfactory-loader';import {AppModule} from './app.module';import {AppComponent} from './app.component';
@NgModule({ imports: [ AppModule, ServerModule, ModuleMapLoaderModule, //server specific imports ], bootstrap: [AppComponent],})export class AppServerModule {}
app.browser.module.ts @NgModule({ bootstrap: [AppComponent], imports: [ AppModule, BrowserModule.withServerTransition({appId: 'my-app'}), //browser specific imports ]})export class AppModule {}
//browser specific imports
//server specific imports
SSRServer vs.
ng add @ng-toolkit/universal
CREATE local.js (248 bytes) CREATE server.ts (1546 bytes) CREATE webpack.server.config.js (1214 bytes) CREATE src/main.server.ts (249 bytes) CREATE src/tsconfig.server.json (485 bytes) CREATE src/app/app.browser.module.ts (395 bytes) CREATE src/app/app.server.module.ts (788 bytes) CREATE ng-toolkit.json (95 bytes) UPDATE package.json (1840 bytes) UPDATE angular.json (4022 bytes) UPDATE src/app/app.module.ts (417 bytes) UPDATE src/main.ts (447 bytes)
SSRServer vs.
And let’s go!
• npm run build:prod
• npm run server
Date: 2018-11-21T13:04:33.302Z Hash: 1a82cb687d2e22b5d12b Time: 10752ms chunk {0} runtime.ec2944dd8b20ec099bf3.js (runtime) 1.41 kB [entry] [rendered] chunk {1} main.09093ffa4ad7f66bc6ff.js (main) 169 kB [initial] [rendered] chunk {2} polyfills.c6871e56cb80756a5498.js (polyfills) 37.5 kB [initial] [rendered] chunk {3} styles.3bb2a9d4949b7dc120a9.css (styles) 0 bytes [initial] [rendered]
> [email protected] server /Users/mtreder/myApp > node local.js
Listening on: http://localhost:8080
SSRServer vs.
Under the hood
export const app = express();
app.use(compression()); app.use(cors()); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true }));
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main');
app.engine('html', ngExpressEngine({ bootstrap: AppServerModuleNgFactory, providers: [ provideModuleMap(LAZY_MODULE_MAP) ] }));
server.ts
SSRServer vs.
Under the hood
app.get('/*', (req, res) => { res.render('index', {req, res}, (err, html) => { if (html) { res.send(html); } else { console.error(err); res.send(err); } }); });
server.ts
SSRServer vs.
Under the hood
app.set('view engine', 'html'); app.set('views', './dist/browser');
app.get('*.*', express.static('./dist/browser', { maxAge: '1y' }));
server.ts
SSRServer vs.
Server Side Rendering
GET /GET /anotherPage
SSRServer vs.
ng-toolkit
SSRServer vs.
Browser vs. Server
• document
• window
• navigator
• file system
• request
Server vs. Browser
Browser vs. Serverpublic ngOnInit(): void { console.log(window.navigator.language); }
Listening on: http://localhost:8080 ERROR ReferenceError: window is not defined at AppComponent.module.exports../src/app/app.component.ts.AppComponent.ngOnInit (/Users/mtreder/myApp/dist/server.js:118857:21) at checkAndUpdateDirectiveInline (/Users/mtreder/myApp/dist/server.js:19504:19) at checkAndUpdateNodeInline (/Users/mtreder/myApp/dist/server.js:20768:20) at checkAndUpdateNode (/Users/mtreder/myApp/dist/server.js:20730:16) at prodCheckAndUpdateNode (/Users/mtreder/myApp/dist/server.js:21271:5) at Object.updateDirectives (/Users/mtreder/myApp/dist/server.js:118833:264) at Object.updateDirectives (/Users/mtreder/myApp/dist/server.js:21059:72) at Object.checkAndUpdateView (/Users/mtreder/myApp/dist/server.js:20712:14) at ViewRef_.module.exports.ViewRef_.detectChanges (/Users/mtreder/myApp/dist/server.js:19093:22) at /Users/mtreder/myApp/dist/server.js:15755:63
Server vs. Browser
server? browser?import { Component, Inject, PLATFORM_ID, OnInit } from '@angular/core'; import { isPlatformBrowser, isPlatformServer } from '@angular/common';
@Component({ selector: 'home-view', templateUrl: './home.component.html' }) export class HomeComponent implements OnInit {
constructor( private platformId) {} public ngOnInit(): void { if ( ) { console.log('I am executed in the browser!’); // window.url can be reached here }
if (isPlatformServer(this.platformId)) { console.log('I am executed in the server!’); // window.url CAN’T be reached here } } }
Server vs. Browser
Wrapper Service
• Determine if we are in the browser or server
• Retrieve window or request object
• Create ‘mock’ window based on request object if necessary
Server vs. Browser
REQUESTimport { Component, OnInit, Inject, PLATFORM_ID, Optional } from ‘@angular/core’; import { REQUEST } from '@nguniversal/express-engine/tokens'; import { isPlatformServer } from '@angular/common';
@Component({ selector: 'app-root', templateUrl: './app.component.html', }) export class AppComponent implements OnInit {
constructor( @Inject(REQUEST) private request: any, @Inject(PLATFORM_ID) private platformId: any) {} public ngOnInit(): void { if (isPlatformServer(this.platformId)) { console.log(this.request.headers); } } }
import { REQUEST } from '@nguniversal/express-engine/tokens';
@Optional @Inject(REQUEST) private request: any,
console.log(this.request.headers);
Server vs. Browser
REQUEST
Listening on: http://localhost:8080 { host: 'localhost:8080', connection: 'keep-alive', 'cache-control': 'max-age=0', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36', accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'en-US,en;q=0.9,ru;q=0.8', 'if-none-match': 'W/"40e-JviTST4QyiABJz2Lg+QxzZtiXv8"' } 'accept-language': 'en-US,en;q=0.9,ru;q=0.8',
Server vs. Browser
Wrapper Service@Injectable() export class WindowService { private _window: Window; constructor(@Inject(PLATFORM_ID) platformId: any, @Optional @Inject(REQUEST) private request: any ) { if (isPlatformServer(platformId)) { this._window = { navigator: { language: this.request.headers['accept-language'] }, URL: this.request.headers.host + '' + this.request.url };
} else { this._window = window; } }
get window(): any { return this._window; } }
Server vs. Browser
Wrapper Service
import { Component , OnInit, Inject} from '@angular/core'; import { WINDOW } from '@ng-toolkit/universal';
export class AppComponent implements OnInit {
constructor(@Inject(WINDOW) private window: Window) {}
public ngOnInit(): void { console.log(window.navigator.language); } }
app.component.ts
console.log(this.window.navigator.language);
Server vs. Browser
@ng-toolkit/universal
import { NgtUniversalModule } from '@ng-toolkit/universal'; import { NgModule } from '@angular/core';
@NgModule({ imports:[ NgtUniversalModule ] }) export class AppModule { }
app.module.ts
Server vs. Browser
Server/Browser modules
@ngx-translate
• i18n module
• multiple ways of usage
{{‘Welcome to' | translate}}
<div [innerHTML]="'HELLO' | translate"></div>
{
"Welcome to": "Ласкаво просимо в"
}
uk.json
Server vs. Browser
Server/Browser modules
import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http); }
@NgModule({ imports:[ TranslateModule.forRoot({ loader: {provide: TranslateLoader, useFactory: HttpLoaderFactory, deps: [httpClient]} }) ] }) export class AppBrowserModule {}
export function httpLoaderFactory(http: HttpClient): TranslateLoader { return new TranslateHttpLoader(http); }
HttpLoaderFactory,
app.browser.module.ts
Server vs. Browser
Server/Browser modulesimport { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { Observable, Observer } from 'rxjs'; import * as fs from 'fs';
export function universalLoader(): TranslateLoader { return { getTranslation: (lang: string) => { return Observable.create((observer: Observer<any>) => { observer.next(JSON.parse(fs.readFileSync(`./dist/assets/i18n/${lang}.json`, 'utf8'))); observer.complete(); }); } } as TranslateLoader; }
@NgModule({ imports:[ TranslateModule.forRoot({ loader: {provide: TranslateLoader, useFactory: universalLoader} }) ] }) export class AppServerModule {}
export function universalLoader(): TranslateLoader {
app.server.module.ts
universalLoader }
Server vs. Browser
Server/Browser modulesimport { Component, OnInit, Inject } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { WINDOW } from '@ng-toolkit/universal';
@Component({ selector: 'app-root', templateUrl: './app.component.html', }) export class AppComponent implements OnInit {
constructor( @Inject(WINDOW) private window: Window, private translateService: TranslateService ) {} public ngOnInit(): void { this.translateService.use(this.window.navigator.language); } }
app.component.ts
this.translateService.use(this.window.navigator.language);
Server vs. Browser
i18n with Universalhttps://www.twilio.com/blog/create-search-engine-friendly-internationalized-web-apps-angular-universal-ngx-translate
Server vs. Browser
Server vs. Browser
API optimization
Server vs. APIs
DRY(c)Don’t repeat your calls
export class AppComponent implements OnInit {
public post: Observable<any>;
constructor(private httpClient: HttpClient) {}
public ngOnInit(): void { this.post = this.httpClient.get('https://jsonplaceholder.typicode.com/posts/1'); } }
Server vs. APIs
2
1
3
45 6
external.api.com
Server vs. APIs
HttpCacheModulenpm install @nguniversal/common
import { NgtUniversalModule } from '@ng-toolkit/universal'; import { CommonModule } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; import { TransferHttpCacheModule } from '@nguniversal/common'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component';
@NgModule({ declarations: [ AppComponent ], imports:[ CommonModule, NgtUniversalModule, TransferHttpCacheModule, HttpClientModule ] }) export class AppModule { }
Server vs. APIs
TransferState
• ServerTransferStateModule (@angular/platform-server)
• BrowserTransferStateModule (@angular/platform-browser)
• get(key, fallbackValue)
• set(key, value)
• has(key)
• remove(key)
Server vs. APIs
HTTP_INTERCEPTOR
• Provided in the AppModule
• Every http request made with HttpClient goes threw it
• Used to transform request or response ie:
• Adding authentication headers
Server vs. APIs
HTTP_INTERCEPTOR
@Injectable() export class ServerStateInterceptor implements HttpInterceptor { constructor(private _transferState: TransferState) {} intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(req).pipe(tap(event => { if (event instanceof HttpResponse) { this._transferState.set(makeStateKey(req.url), event.body); } })); } }
Server vs. APIs
HTTP_INTERCEPTOR@Injectable() export class BrowserStateInterceptor implements HttpInterceptor { constructor(private _transferState: TransferState) { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { if (req.method !== 'GET') { return next.handle(req); } const storedResponse: string = this._transferState.get(makeStateKey(req.url), null); if (storedResponse) { const response = new HttpResponse({ body: storedResponse, status: 200 }); this._transferState.remove(makeStateKey(req.url)); return of(response); } return next.handle(req); } }
Server vs. APIs
HTTP_INTERCEPTORimport {HTTP_INTERCEPTORS } from '@angular/common/http';
providers: [ { provide: HTTP_INTERCEPTORS, useClass: BrowserStateInterceptor, multi: true, } ]
import {HTTP_INTERCEPTORS } from '@angular/common/http';
providers: [ { provide: HTTP_INTERCEPTORS, useClass: ServerStateInterceptor, multi: true, } ]
Server vs. APIs
Performanceexport class RouteResolverService implements Resolve<any> {
constructor( private httpClient: HttpClient, @Inject(PLATFORM_ID) private platformId: any ) {}
public resolve(): Observable<any> {
} }
const watchdog: Observable<number> = timer(500);
if (isPlatformBrowser(this.platformId)) { return this.httpClient.get<any>('https://jsonplaceholder.typicode.com/posts/1'); }
return Observable.create(subject => { this.httpClient.get<any>('https://jsonplaceholder.typicode.com/posts/1') .subscribe(response => { subject.next(response); subject.complete(); });
})
.pipe(takeUntil(watchdog))
watchdog.subscribe(() => { subject.next('timeout'); subject.complete() })
Server vs. APIs
0.5
sec
Server vs. APIs
0.5
sec
Server vs. APIs
https://www.twilio.com/blog/faster-javascript-web-apps-angular-universal-transferstate-api-watchdog
DRY(c) & Performance
Server vs. APIs
Server vs. APIs
Deployment
Server vs. Deploy
Let’s go Serverless!
• Function as a Service
• Event-driven
• Scalable
• Pay for the up-time
Server vs. Deploy
Let’s go Serverless!https://www.twilio.com/blog/angular-universal-javascript-node-js-aws-lambda
Server vs. Deploy
Server vs. Deploy
Prerender
• Generating HTML files at a build time
• Can be hosted from traditional hosting (ie. AWS S3)
• Doesn’t perform dynamic request
• https://github.com/maciejtreder/angular-ssr-prerender
• @ng-toolkit/universal + npm run build:prerender
Server vs. Deploy
Summaryserver-side renderingprerenderng build —prod
SEO
Performance
Difficulty
SEO + external calls
Additional back-end logic
Server vs. Summary
@maciejtreder