ANGULAR UNIVERSAL

in

2019

Hi!

I'm

Craig

2019

This is a deep dive!

@phenomnominal

Assumptions:

You know some Angular 

You've heard of Angular Universal 

 What we're covering:

How does Angular Universal work?

What problems does it solve?

How do we make our applications work with Universal?

Why would you not want to use Universal?

@phenomnominal
2019

how does Angular Universal work?

 Angular

thereal.world/cast/:id
  <html>
      <link 
          rel="stylesheet"
          href="/static/styles.css">
      <script
          src="/static/script.js">
      <body>
          <the-real-world>
              <div
                  class="trw-loading">
              </div>
          </the-real-world>
      </body>
  </html>
          rel="stylesheet"
          href="/static/styles.css"

          src="/static/script.js"



                  class="trw-loading"


              "stylesheet"
               "/static/styles.css"

              "/static/script.js"



                        "trw-loading"


Generic application loading state

Static resources

@phenomnominal
2019
thereal.world/static/script.js
thereal.world/static/styles.css
                () {
           ...
       }
       
            { 
           ...
       }
                
           ...
       
       
             
           ...
       
       function () {
           ...
       }
       
       html { 
           ...
       }

 Angular

@phenomnominal
2019
thereal.world/api/seasons.json
thereal.world/api/cast/:id.json

 Angular

{
    "id": "1234567890", ...
}

{
    "seasons": [{
        "year": 1992,
        "city": "New  York", ...
    }]
}
@phenomnominal
2019

What can we do to remove some of those round trips?

@phenomnominal
2019

 Angular

 Universal

@phenomnominal
2019

 Angular

 Universal

thereal.world/cast/:id
  <html>
      <link 
          rel="stylesheet"
          href="/static/styles.css">
      <script
          src="/static/script.js">
      <body>
          <the-real-world>
              <trw-homepage>
                  <trw-search>
                  <trw-seasons>
              </trw-homepage>
          </the-real-world>
      </body>
      <script
          id="server-app-state">
      </script>
</html>
              "stylesheet"
               "/static/styles.css"

              "/static/script.js"

          







             "server-app-state"

          rel="          "
          href="                  "

          src="                 "









          id=

Real application content!

Inlined server request data!

@phenomnominal
2019
@phenomnominal

What problems does it solve?

Performance

SEO/No JavaScript environments

Social media links

 Angular

 Universal

2019

  Performance

Slower "Time to First Byte" due to server processing time

Faster "First Paint" over client-only application, particularly for mobile devices

This can be mitigated with caching - both on the server & on the client with a service worker

Slower "Time to Interactive" due to client-side rehydration

Faster discovery of resources required for your application

Measure, experiment, and find what works for your app

@phenomnominal
2019

SEO/No JS 

Some crawlers have JavaScript capabilities, but who knows how that really works (not me!)

Support for browsers with JavaScript disabled, or old engines.

Check out Igor Minar's  talk from ng-conf 2018 for how angular.io handles SEO without Angular Univeral

If you're competing against other websites, server rendering can give you an advantage over client-only SEO

@phenomnominal
2019

Social links

Pre-rendered content for Facebook, Twitter, etc.

@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019

How do we make our

 

applications work with Angular Universal?

REAL WORLD!

@phenomnominal
2019
@phenomnominal
2019

We can rebuild it!

@phenomnominal
2019
@phenomnominal
2019

 Angular CLI

ng new the-real-world && cd the-real-world
ng add @nguniversal/express-engine --clientProject the-real-world
npm run build:ssr
npm run serve:ssr

A new app Angular CLI app

Build steps for two different versions of the app

All the necessary Angular Universal dependencies

A new server.ts file which contains the server code

A modified app.module.ts that uses withServerTransition

A new app.server.module.ts file for the server application

A main.ts that waits for DOMContentLoaded before bootstrap

A new main.server.ts file for the server bootstrap

@phenomnominal
2019

Build steps for server

@phenomnominal
2019

Demo!

@phenomnominal
2019

JavaScript runtime (no DOM)

Driven by Chrome's V8 engine

Use everyone else's code via 

@phenomnominal
2019

Require the built Universal app

Set up the engine for running the application

Pass all requests to the engine

Create the Express server

Start the server

@phenomnominal
2019

 Node.js

const app = express();

const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main');

app.engine('html', ngExpressEngine({
  bootstrap: AppServerModuleNgFactory,
  providers: [
    provideModuleMap(LAZY_MODULE_MAP)
  ]
}));

app.set('view engine', 'html');
app.set('views', DIST_FOLDER);

app.get('*.*', express.static(DIST_FOLDER, {
  maxAge: '1y'
}));

app.get('*', (req, res) => {
  res.render('index', { req });
});

app.listen(PORT, () => {
  console.log(`Node Express server listening on http://localhost:${PORT}`);
});
@phenomnominal
2019

TIME TRAVEL

@phenomnominal
2019

Demo!

@phenomnominal
2019

⭐️ @angular/cli application

⭐️ Beautiful 90s inspired table layouts

⭐️ Cast member data requested from API

⭐️ D3 rendered SVG world map

⭐️ @ngrx/store + router-state + entity

😔 Client render only

@phenomnominal
2019

document is not defined

@phenomnominal
2019

How do we run this on the server?

@phenomnominal
2019

Run your Angular application anywhere...

https://www.youtube.com/watch?v=_trUBHaUAR0
@phenomnominal
2019

 Angular

 Universal

anywhere!

@phenomnominal
2019
@angular/platform-browser
@angular/platform-browser-dynamic
@angular/platform-server
@angular/platform-webworker // Gone!!
@angular/platform-webworker-dynamic // Gone!!
@angular/platform-terminal // Maybe??

How do we run this anywhere?

@phenomnominal
2019

One implementation for all environments

One implementation for a specific environment

Different implementations for each environment

Different functionality for each environment

 Four patterns

@phenomnominal
2019

One implementation for all environments

@phenomnominal
2019

Avoiding the DOM

Do not use the DOM directly! There's almost always a way to do the same thing with Angular's abstractions.

@phenomnominal
2019

Components

@Input, @Output, @HostBinding, @ViewChild, @ContentChild, ngStyle, ngClass.

You may occasionally need to reach for the Renderer:

Normal dependency injection

@phenomnominal
2019
import { Component, Renderer2 } from '@angular/core'

@Component({
    // ...
})
export class MyComponent {
    constructor (
        private _renderer: Renderer2
    ) { }
}

But what if you have to use the DOM?

@phenomnominal
2019

Be explicit about it!

Be defensive!

Explicit null type is good

Only run code if window is available.

@phenomnominal
2019
@Injectable({
    providedIn: 'root'
})
export class WindowRef<T extends Window = Window> {
    private readonly _window: T | null;

    constructor () {
        this._window = this._getWindow();
    }

    public useWindow <U> (handler: (window: T) => U): U | null {
        return this._window ? handler(this._window): null;
    }

    public _getWindow (): T | null {
        return typeof window !== 'undefined' ? window as T : null;
    }
}

Provide an alternative

Use custom services to abstract around DOM APIs.

Fall back to the actual DOM

Optional injection token!

Use the optional value

@phenomnominal
2019
export const URL_LOCATION_TOKEN = new InjectionToken('URL_LOCATION');

@Injectable({
    // ...
})
export class UrlLocationService {
    constructor (
        private _window: WindowRef,
        @Optional() @Inject(URL_LOCATION_TOKEN) private _location: Location
    ) {
        this._location = this._location || this._window.useWindow(w => w.location);
    }

    public getHostname (): string {
        return this._location.hostname;
    }
}

Hack it?

Some third-party code won't want to play nicely...

(*cough* three-trackballcontrols *cough*)

Set up globals

Require troublesome module

Clean up after

Directly check for window

@phenomnominal
2019
if (typeof window === 'undefined') {
    (global as any).window = {};
    require('three-trackballcontrols');
    delete (global as any).window;
}

Check the platform...

As a last resort (please!)

@phenomnominal
2019

If you really must...

Injectable PLATFORM_ID token

Use the method for the specific platform

Check the platform...

@phenomnominal
2019
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
import { Inject, PLATFORM_ID } from '@angular/core';

@Component({
    // ...
})
export class MyComponent {
    constructor (
        @Inject(PLATFORM_ID) private _platformId: Object
    ) { }

    public myExpensiveDomHeavyOperation (): void {
        if (!isPlatformBrowser(this._platformId) {
            return;
        }

        this._useTheDom();
    }
}

One implementation for a specific environment

@phenomnominal
2019

Add @Optional injection annotation

Trusty if statement

Check for the presence of an injected service

Just turn it off

@phenomnominal
2019
@Component({
    // ...
})
export class MapComponent implements AfterViewInit {
    @ViewChild('map', { static: true }) public map: ElementRef;

    constructor (
        @Optional() private _mapService: MapService
    ) { }

    public ngAfterViewInit (): void {
        if (!this._mapService) {
            return;
        }


        this._init();
    }
}

Turn things off at a provider level

Provide null for specific platform

Corresponding injection token

@phenomnominal
2019

Just turn it off

@NgModule({
    imports: [
        // ...
    ],
    bootstrap: [AppComponent],
    providers: [
        { provide: MapService, useValue: null }
    ]
})
export class AppServerModule { }
@phenomnominal
2019

Demo!

@phenomnominal
2019
@phenomnominal
2019

😕 data requested from API two times 

⭐️ No errors!

😕 flickery re-render at after bootstrap

⭐️ Fast!

Different implementations for each environment

@phenomnominal
2019

Platform-specific modules

Remember app.module.ts & app.server.module.ts?

@phenomnominal
2019
// app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';

@NgModule({
    bootstrap: [AppComponent],
    declarations: [AppComponent],
    imports: [
        BrowserModule.withServerTransition({ appId: 'serverApp' }),
        RouterModule.forRoot([{
            // routes
        }], {
            initialNavigation: 'enabled'
        })
    ]
})
export class AppModule { }
// app.server.module.ts

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({
    bootstrap: [AppComponent],
    imports: [
        AppModule,
        ModuleMapLoaderModule,
        ServerModule
    ]
})
export class AppServerModule { }

 Services

Provide different implementations for different environments:

Server implementation for Renderer

Server implementation for HttpClient

@phenomnominal
2019
// @angular/platform-server

@NgModule({
    exports: [BrowserModule],
    imports: [HttpModule, HttpClientModule, NoopAnimationsModule],
    providers: [
        SERVER_RENDER_PROVIDERS,
        SERVER_HTTP_PROVIDERS,
        // ...
    ] 
})
export class ServerModule { }
@phenomnominal
2019

Different functionality for each environment

The whole application runs twice.

This means duplicate API calls!

This call happens twice

@phenomnominal
2019

State Transfer

@Component({
    // ...
})
export class AppComponent implements OnInit {
    public castMember: CastMember;

    constructor (
        private _castMemberDataService: CastMemberDataService
    ) { }

    public ngOnInit (): void {
        this._castMemberDataService.getCastMember()
            .subscribe(data => this.castMember = data);
    }
}

Inject TransferState

Create a state key

Set the state on the first run

Re-use it

@phenomnominal
2019

State Transfer

import { TransferState, makeStateKey } from '@angular/platform-browser';

const CAST_MEMBER = makeStateKey('castMember');

@Component({
    // ...
})
export class AppComponent implements OnInit {
    public castMember: CastMember

    constructor (
        private _castMemberDataService: CastMemberDataService,
        private _state: TransferState
    ) { }

    public ngOnInit (): void {
        this.castMember = this.state.get(CAST_MEMBER, null);

        if (!this.castMember) {
            this._castMemberDataService.getCastMember()
                .subscribe(data => {
                    this.castMember = data;
                    this.state.set(CAST_MEMBER, data);
                });
        }
    }
}

Or just transfer your whole store state!

One state key for your whole store

Read the whole store and serialise

@phenomnominal
2019

State Transfer

@NgModule({
    imports: [BrowserTransferStateModule]
})
export class ServerStateModule {
    constructor (
        private _transferState: TransferState,
        private _store: Store<State>
    ) {
        this._transferState.onSerialize(STATE_KEY, () => {
            let stateToTransfer = null;
            this._store.pipe(
                select(state => state),
                take(1)
            ).subscribe(state => stateToTransfer = state);

            return stateToTransfer;
        });
    }
}

Use the same key on the client

Dispatch a new action with the whole serialised store 

@phenomnominal
2019

State Transfer

Or just transfer your whole store state!

@NgModule({
    imports: [BrowserTransferStateModule]
})
export class ClientStateModule {
    constructor (
        private _transferState: TransferState,
        private _store: Store<State>
    ) {
        if (this._transferState.hasKey(STATE_KEY)) {
            const state = this._transferState.get<State>(STATE_KEY, {});
            this._transferState.remove(STATE_KEY);

            this.store.dispatch(new TransferStateAction(state));
        }
    }
}

Use a simple cache layer to prevent duplicated calls

Split the GET efffect into two parts

Mark each API response with a cache time

Only GET if the cache has expired

@phenomnominal
2019

State Transfer

@Effect()
public getCastMemberDataEffect$ = this._actions.pipe(
    ofType<GetCastMemberDataAction>(GetCastMemberDataAction.TYPE),
    switchMap(action => 
        this._store.pipe(
            select(CastMemberDataSelectors.currentCastMemberData),
            take(1),
            filter(storeItem => this._cacheService.shouldFetch(storeItem, { expiryTime: 120000 })),
            map(() => new GetCastMemberDataFromApi(action.options)
        )
    )

@Effect()
public getCastMemberDataFromApiEffect$ = this._actions.pipe(
    ofType<GetCastMemberDataFromApi>(GetCastMemberDataFromApi.TYPE),
    switchMap(action => 
        this._castMemberDataService.getAsteroidData(action.options).pipe(
            map(() => new GetCastMemberDataFromApiSucccess(action.options, response, new Data().toString())),
            catchError(err => of(new GetCastMembersDataFromApiFail(actions, options, err)))
        )
    )

The state transfer data is inlined into the HTML response as a blob of JSON. This means...

Don't send too much data!

The whole store must be  JSON serialisable!

@phenomnominal
2019

State Transfer

@phenomnominal
2019

Demo!

@phenomnominal
2019

One implementation for all environments

One implementation for a specific environment

Different implementations for each environment

Different functionality for each environment

COOL.

@phenomnominal
2019

Why wouldn't you use Angular Universal?

@phenomnominal
2019

 Performance

Universal is not a magical silver rocket.

If performance is your only goal, just ship less JavaScript

It can definitely make things worse if you're not careful!

@phenomnominal
2019

 Complexity

You now need a "real" server, not just static files.

Harder to get working, harder mental model, harder to get people shipping value

There will be lots more code - logging, monitoring

@phenomnominal
2019

It's very slow...

Bazel will fix it?

@phenomnominal
2019

WorkFlow/DX

So it's just a dumpster fire?

Lint rules for using DOM APIs

Lint rules for using isBrowser

Meta-reducer for unserialisable data

Test for too much serialised data

Universal E2E tests

Fix it before you break it

@phenomnominal
2019
@phenomnominal
2019

Should i Use Angular UniversaL?

If you want/need your Angular app to be crawled by Search Engines

@phenomnominal
2019

If you want/need improved No JS/old browser support

@phenomnominal
2019

If you want/need content previews for Social Media

@phenomnominal
2019

AND if you're willing to fight for better perceived performance

@phenomnominal
2019

 Angular

 Universal

@phenomnominal
2019
@phenomnominal
2019
myspace/phenomnominal
geocities/phenomnomnominal

Angular Universal in the Real World

By Craig Spence

Angular Universal in the Real World

Craig Spence - Angular UP 2019 - Angular Universal in the Real World

  • 3,358