Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Angular Universal - A medicine for the SEO/CDN issues

Angular Universal - A medicine for the SEO/CDN issues

Presentation from the OdessaJS about Angular Universal and basic techniques of Angular Server-Side code.

Maciej Treder

July 21, 2019
Tweet

More Decks by Maciej Treder

Other Decks in Programming

Transcript

  1. Outline • SPA pitfall • Server-side rendering • Server vs.

    Browser • API optimization • Deployment • Prerendering & Summary SPA pitfall SSR Server vs. Browser APIs Deploy Summary
  2. 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 pitfall
  3. 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,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
  4. SSR

  5. —prod vs. universal Load HTML Bootstrap Load HTML Bootstrap SSR

    NO SSR First meaningful paint First meaningful paint SSR
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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 SSR
  11. 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
  12. 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
  13. Browser vs. Server • document • window • navigator •

    file system • request Server vs. Browser
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. @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
  22. 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
  23. 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
  24. 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<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
  25. 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
  26. 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'); } } APIs
  27. 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
  28. 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
  29. 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); } })); } } APIs
  30. 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); } } APIs
  31. 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
  32. Performance export 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() }) APIs
  33. Let’s go Serverless! • Function as a Service • Event-driven

    • Scalable • Pay for the up-time Deploy
  34. 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
  35. Summary server-side rendering prerender ng build —prod SEO Performance Difficulty

    SEO + external calls Additional back-end logic Summary