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. Summer. Sea. JavaScript.
    Angular Universal
    A medicine for the SEO/CDN issues
    Maciej
    Treder
    @maciejtreder

    View Slide

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

    View Slide

  3. SPA pitfall
    ng build
    • Ahead of Time compilation

    View Slide

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

    View Slide

  5. ng build vs. —prod
    SPA pitfall

    View Slide

  6. SPA problem

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

    .htaccess
    SPA pitfall

    View Slide

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

    View Slide

  8. SPA Problem
    GET / GET /anotherPage
    SPA pitfall

    View Slide

  9. SPA Problem
    SPA pitfall

    View Slide

  10. Server Side Rendering
    SSR

    View Slide

  11. Server Side Rendering
    GET /
    GET /anotherPage
    SSR

    View Slide

  12. Is it worth?
    curl localhost:8080


    SomeProject



    <br/>/*# sourceMappingURL=data:application/<br/>json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsImZpbGUiOiJzcmMvYXBwL2FwcC5jb21wb25lbnQuY3NzIn0= */</<br/>style></head><br/><body><br/><script type="text/javascript" src="runtime.26209474bfa8dc87a77c.js"></script><script type="text/javascript" src="es2015-<br/>polyfills.c5dd28b362270c767b34.js" nomodule=""></script><script type="text/javascript" src="polyfills.8bbb231b43165d65d357.js"></<br/>script><script type="text/javascript" src="main.8a9128130a3a38dd7ee5.js"></script><br/><script id="app-root-state" type="application/json">{}</script></body></html><br/><app-root _nghost-sc0="" ng-version="7.2.9"><div _ngcontent-sc0="" style="text-align:center"><h1 _ngcontent-sc0=""> Welcome<br/>to someProject! </h1><img _ngcontent-sc0="" alt="Angular Logo" src="data:image/<br/>svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERD<br/>AwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJ<br/>GIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1<br/>Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogI<br/>Dwvc3ZnPg==" width="300"></div><h2 _ngcontent-sc0="">Here are some links to help you start: </h2><ul _ngcontent-sc0=""><li<br/>_ngcontent-sc0=""><h2 _ngcontent-sc0=""><a _ngcontent-sc0="" href="https://angular.io/tutorial" rel="noopener"<br/>target="_blank">Tour of Heroes</a></h2></li><li _ngcontent-sc0=""><h2 _ngcontent-sc0=""><a _ngcontent-sc0="" href="https://<br/>angular.io/cli" rel="noopener" target="_blank">CLI Documentation</a></h2></li><li _ngcontent-sc0=""><h2 _ngcontent-sc0=""><a<br/>_ngcontent-sc0="" href="https://blog.angular.io/" rel="noopener" target="_blank">Angular blog</a></h2></li></ul></app-root><br/>SSR<br/>

    View Slide

  13. SSR

    View Slide

  14. —prod vs. universal
    SSR

    View Slide

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

    View Slide

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

    View Slide

  17. 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

    View Slide

  18. 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

    View Slide

  19. 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

    View Slide

  20. 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

    View Slide

  21. 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

    View Slide

  22. 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

    View Slide

  23. 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

    View Slide

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

    View Slide

  25. Server Side Rendering
    GET /
    GET /anotherPage
    SSR

    View Slide

  26. ng-toolkit
    SSR

    View Slide

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

    View Slide

  28. 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

    View Slide

  29. 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

    View Slide

  30. 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

    View Slide

  31. 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

    View Slide

  32. 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

    View Slide

  33. 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

    View Slide

  34. 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

    View Slide

  35. @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

    View Slide

  36. Server/Browser modules
    @ngx-translate
    • i18n module
    • multiple ways of usage
    {{‘Welcome to' | translate}}

    {
    "Welcome to": "Ласкаво просимо в"
    }
    uk.json
    Server vs.
    Browser

    View Slide

  37. 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

    View Slide

  38. 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

    View Slide

  39. 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

    View Slide

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

    View Slide

  41. Server vs.
    Browser

    View Slide

  42. API optimization
    APIs

    View Slide

  43. 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

    View Slide

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

    View Slide

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

    View Slide

  46. 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

    View Slide

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

    View Slide

  48. 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

    View Slide

  49. 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

    View Slide

  50. 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

    View Slide

  51. 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

    View Slide

  52. 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

    View Slide

  53. 0.5 sec
    APIs

    View Slide

  54. 0.5 sec
    APIs

    View Slide

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

    View Slide

  56. APIs

    View Slide

  57. Deployment
    Deploy

    View Slide

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

    View Slide

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

    View Slide

  60. Deploy

    View Slide

  61. 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

    View Slide

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

    View Slide

  63. @maciejtreder

    View Slide