Slide 1

Slide 1 text

Summer. Sea. JavaScript. Angular Universal A medicine for the SEO/CDN issues Maciej Treder @maciejtreder

Slide 2

Slide 2 text

Outline • SPA pitfall • Server-side rendering • Server vs. Browser • API optimization • Deployment • Prerendering & Summary SPA pitfall SSR Server vs. Browser APIs Deploy Summary

Slide 3

Slide 3 text

SPA pitfall ng build • Ahead of Time compilation

Slide 4

Slide 4 text

—prod flag • Ahead of Time compilation • Minified • Tree-shaked SPA pitfall

Slide 5

Slide 5 text

ng build vs. —prod SPA pitfall

Slide 6

Slide 6 text

SPA problem RewriteEngine On RewriteBase / RewriteRule ^index\.html$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . index.html [L] .htaccess SPA pitfall

Slide 7

Slide 7 text

SPA Problem GET / GET /anotherPage index.html GET /subpage GET /contact GET /home SPA pitfall

Slide 8

Slide 8 text

SPA Problem GET / GET /anotherPage SPA pitfall

Slide 9

Slide 9 text

SPA Problem SPA pitfall

Slide 10

Slide 10 text

Server Side Rendering SSR

Slide 11

Slide 11 text

Server Side Rendering GET / GET /anotherPage SSR

Slide 12

Slide 12 text

Is it worth? curl localhost:8080 SomeProject /*# 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,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERD AwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJ GIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1 Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogI Dwvc3ZnPg==" 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> SSR

Slide 13

Slide 13 text

SSR

Slide 14

Slide 14 text

—prod vs. universal SSR

Slide 15

Slide 15 text

—prod vs. universal Load HTML Bootstrap Load HTML Bootstrap SSR NO SSR First meaningful paint First meaningful paint SSR

Slide 16

Slide 16 text

How to start? Official guide https://angular.io/guide/universal ng-toolkit https://github.com/maciejtreder/ng-toolkit SSR

Slide 17

Slide 17 text

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) SSR

Slide 18

Slide 18 text

Adjust your modules app.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 {} SSR

Slide 19

Slide 19 text

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 SSR

Slide 20

Slide 20 text

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) SSR

Slide 21

Slide 21 text

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] > my-app@0.0.0 server /Users/mtreder/myApp > node local.js Listening on: http://localhost:8080 SSR

Slide 22

Slide 22 text

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 SSR

Slide 23

Slide 23 text

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 SSR

Slide 24

Slide 24 text

Under the hood app.set('view engine', 'html'); app.set('views', './dist/browser'); app.get('*.*', express.static('./dist/browser', { maxAge: '1y' })); server.ts SSR

Slide 25

Slide 25 text

Server Side Rendering GET / GET /anotherPage SSR

Slide 26

Slide 26 text

ng-toolkit SSR

Slide 27

Slide 27 text

Browser vs. Server • document • window • navigator • file system • request Server vs. Browser

Slide 28

Slide 28 text

Browser vs. Server public 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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

REQUEST import { 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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

@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

Slide 36

Slide 36 text

Server/Browser modules @ngx-translate • i18n module • multiple ways of usage {{‘Welcome to' | translate}}
{ "Welcome to": "Ласкаво просимо в" } uk.json Server vs. Browser

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

Server/Browser modules import { 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) => { 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

Slide 39

Slide 39 text

Server/Browser modules import { 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

Slide 40

Slide 40 text

i18n with Universal https://www.twilio.com/blog/create-search-engine-friendly- internationalized-web-apps-angular-universal-ngx-translate Server vs. Browser

Slide 41

Slide 41 text

Server vs. Browser

Slide 42

Slide 42 text

API optimization APIs

Slide 43

Slide 43 text

DRY(c) Don’t repeat your calls export class AppComponent implements OnInit { public post: Observable; constructor(private httpClient: HttpClient) {} public ngOnInit(): void { this.post = this.httpClient.get('https://jsonplaceholder.typicode.com/posts/1'); } } APIs

Slide 44

Slide 44 text

2 1 3 4 5 6 external.api.com APIs

Slide 45

Slide 45 text

external.api.com my-website.com 1 2 3 APIs

Slide 46

Slide 46 text

HttpCacheModule npm 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 { } APIs

Slide 47

Slide 47 text

TransferState • ServerTransferStateModule (@angular/platform-server) • BrowserTransferStateModule (@angular/platform-browser) • get(key, fallbackValue) • set(key, value) • has(key) • remove(key) APIs

Slide 48

Slide 48 text

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 APIs

Slide 49

Slide 49 text

HTTP_INTERCEPTOR @Injectable() export class ServerStateInterceptor implements HttpInterceptor { constructor(private _transferState: TransferState) {} intercept(req: HttpRequest, next: HttpHandler): Observable> { return next.handle(req).pipe(tap(event => { if (event instanceof HttpResponse) { this._transferState.set(makeStateKey(req.url), event.body); } })); } } APIs

Slide 50

Slide 50 text

HTTP_INTERCEPTOR @Injectable() export class BrowserStateInterceptor implements HttpInterceptor { constructor(private _transferState: TransferState) { } intercept(req: HttpRequest, next: HttpHandler): Observable> { 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); } } APIs

Slide 51

Slide 51 text

HTTP_INTERCEPTOR import {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, } ] APIs

Slide 52

Slide 52 text

Performance export class RouteResolverService implements Resolve { constructor( private httpClient: HttpClient, @Inject(PLATFORM_ID) private platformId: any ) {} public resolve(): Observable { } } const watchdog: Observable = timer(500); if (isPlatformBrowser(this.platformId)) { return this.httpClient.get('https://jsonplaceholder.typicode.com/posts/1'); } return Observable.create(subject => { this.httpClient.get('https://jsonplaceholder.typicode.com/posts/1') .subscribe(response => { subject.next(response); subject.complete(); }); }) .pipe(takeUntil(watchdog)) watchdog.subscribe(() => { subject.next('timeout'); subject.complete() }) APIs

Slide 53

Slide 53 text

0.5 sec APIs

Slide 54

Slide 54 text

0.5 sec APIs

Slide 55

Slide 55 text

https://www.twilio.com/blog/faster-javascript-web-apps- angular-universal-transferstate-api-watchdog DRY(c) & Performance APIs

Slide 56

Slide 56 text

APIs

Slide 57

Slide 57 text

Deployment Deploy

Slide 58

Slide 58 text

Let’s go Serverless! • Function as a Service • Event-driven • Scalable • Pay for the up-time Deploy

Slide 59

Slide 59 text

Let’s go Serverless! https://www.twilio.com/blog/angular-universal- javascript-node-js-aws-lambda Deploy

Slide 60

Slide 60 text

Deploy

Slide 61

Slide 61 text

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 Deploy

Slide 62

Slide 62 text

Summary server-side rendering prerender ng build —prod SEO Performance Difficulty SEO + external calls Additional back-end logic Summary

Slide 63

Slide 63 text

@maciejtreder