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 full-size 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 full-size slide

  3. SPA pitfall
    ng build
    • Ahead of Time compilation

    View full-size slide

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

    View full-size slide

  5. ng build vs. —prod
    SPA pitfall

    View full-size 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 full-size slide

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

    View full-size slide

  8. SPA Problem
    GET / GET /anotherPage
    SPA pitfall

    View full-size slide

  9. SPA Problem
    SPA pitfall

    View full-size slide

  10. Server Side Rendering
    SSR

    View full-size slide

  11. Server Side Rendering
    GET /
    GET /anotherPage
    SSR

    View full-size 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 full-size slide

  13. —prod vs. universal
    SSR

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  16. 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 full-size slide

  17. 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 full-size slide

  18. 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 full-size slide

  19. 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 full-size slide

  20. 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 full-size slide

  21. 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 full-size slide

  22. 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 full-size slide

  23. 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 full-size slide

  24. Server Side Rendering
    GET /
    GET /anotherPage
    SSR

    View full-size slide

  25. ng-toolkit
    SSR

    View full-size slide

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

    View full-size slide

  27. 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 full-size slide

  28. 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 full-size slide

  29. 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 full-size slide

  30. 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 full-size slide

  31. 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 full-size slide

  32. 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 full-size slide

  33. 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 full-size slide

  34. @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 full-size slide

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

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

    View full-size slide

  36. 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 full-size slide

  37. 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 full-size slide

  38. 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 full-size slide

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

    View full-size slide

  40. Server vs.
    Browser

    View full-size slide

  41. API optimization
    APIs

    View full-size slide

  42. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

  45. 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 full-size slide

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

    View full-size slide

  47. 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 full-size slide

  48. 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 full-size slide

  49. 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 full-size slide

  50. 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 full-size slide

  51. 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 full-size slide

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

    View full-size slide

  53. Deployment
    Deploy

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  56. 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 full-size slide

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

    View full-size slide

  58. @maciejtreder

    View full-size slide