Angular Universal

Rendering JavaScript on the Server

@MarkPieszak

Hey! I'm Mark Pieszak

* Founder of  http:// DevHelp.Online
   Development / Consulting / Training Firm


   - Helping teams and companies achieve their
     Angular / JavaScript / ASP.NET goals.

 

* Angular Universal team

 -
Working on improving Universal
   & Angular-CLI & ASP.NET integration

Who is this guy?

Mark

Pie

Zack

Szak

Check us out online!
http://DevHelp.Online

Catch me Online!

Github
@MarkPieszak

 

Twitter

@MarkPieszak

Medium

@MarkPieszak
 

DevHelp.Online

http://www.DevHelp.Online
Consulting / Training / Workshops / Development 

Story
Time...

DevHelp.Online

Make sure to push 

to not miss any Slides !

Brief History of the Web

Server-side
Rendered
Websites

The Server-Side

Rendered Web

(Which has improved immensely
since the following screenshots of course!)

aka: SSR

DevHelp.Online

The old Web

DevHelp.Online

The old Web

DevHelp.Online

The old Web

DevHelp.Online

Some things change...
Others stay the same

DevHelp.Online

What does SSR get Right?

Search Engine Optimization

  - Since everything was Server-rendered,
    SEO was never an "issue"...
   
  - It was just something you
    had to add in your code

Great for Static sites

DevHelp.Online

Many other things...

Where does SSR lack sometimes?

Overall "User-Experience"

  - Full page-loads were needed to show new content
    or during any user interaction

  - More server requests because of this

DevHelp.Online

What did the Web do next?

Brief History of the Web

Server-side
Rendered
Websites

Single Page Applications

Single Page Applications

aka: Client-side rendering

( CSR )

DevHelp.Online

More or less...

DevHelp.Online

What does CSR get Right?

A great fully interactive & rich user experience

 

Quick page transitions

Great for web applications

Overall we get that nice
"Perception"
that things are -happening-

DevHelp.Online

Where does CSR fall short?

No Search-engine optimization (at least not easily)

 

DevHelp.Online

Where does CSR fall short?

Longer initial load time

  - Waiting for assets to download

  - Code needs to run

DevHelp.Online

Where do we go from here?

DevHelp.Online

SSR

CSR

DevHelp.Online

SSR

CSR

SEO ✔
Faster initial paint
Social Media Link Previews
Great user experience
Fully interactive

DevHelp.Online

Can we get the best of both worlds?

DevHelp.Online

SSR

CSR

The Birth of
"Isomorphic"
JavaScript

DevHelp.Online

Brief History of the Web

Server-side
Rendered
Websites

Single Page Applications

"Isomorphic" JavaScript

What is Isomorphic JavaScript?

JavaScript
Application
<code />

DevHelp.Online

Same JavaScript Code running
on the Server

Same JavaScript Code running
on the Browser

This was me...

DevHelp.Online

Serialize the application
to a String "<html><head>..."

Your Applications JS code gets downloaded and begins to Bootstrap

( In the background ...)

JavaScript
Application
<code />

DevHelp.Online

serve it to the browser

Application finishes bootstrapping

Now we have a
- fully functioning -
Single Page Application

DevHelp.Online

Angular Universal

DevHelp.Online

DevHelp.Online

Community-driven project originally
Originally created by:

Patrick Stapleton (PatrickJS)
Co-Founder of OneSpeed.io

Jeff Whelpley
CTO GetHuman

Rest of the Universal Team:

Mark Pieszak (me)
DevHelp.Online

Jason Jean
Forbes

Jeff Cross
Nrwl.io

Wassim Chegham
SFEIR

Alex Rickabaugh
Angular Core Team

Vikram Subramanian
Angular Core Team

DevHelp.Online

Moved from a separate repo to
inside Angular Core in 2017

Angular Universal

@angular/
platform-server

renderModuleFactory()

Angular Code bundles
"Bootstraps"
platformBrowser()

( In the background ...)

Angular
Application
<code />

Node Server

App to a String

DevHelp.Online

Angular
Application
<code />

When it finishes Bootstrapping, it replaces existing HTML "App" with Client-side rendered SPA version.


Boom! We have a normal Angular application

DevHelp.Online

Angular-CLI Integration

git clone http://www.github.com/angular/universal-starter

cd universal-starter

npm i 
// or yarn install

// To run Dynamic universal rendering
npm run build:ssr && npm run serve:ssr
// Open localhost:4000

// To run Static "prerendering"
npm run build:prerender && npm run serve:prerender
// Open http://127.0.0.1:8080

DevHelp.Online

- Start Here -
Working Example

http://www.github.com/angular/universal-starter

Angular-CLI Integration

DevHelp.Online

Have an existing CLI Application
you want to make Universal?

 

https://github.com/angular/angular-cli/wiki/stories-universal-rendering

Read the CLI Wiki story on Universal

How does Universal work?

Essentially lots of Dependency Injection

Possible because of Angular's

Compiler & Renderer not being tied to the Browser.

DevHelp.Online

How does Universal work?

Dependency Injection Mania

Provide Use
RenderFactory2 ServerRenderFactory2
BrowserXHR ServerXHR (Zone wrapped)
DOCUMENT Domino
PlatformLocation ServerPlatformLocation
StylesHost ServerStylesHost

DevHelp.Online

Use these instead

How does Universal work?

Angular App

main.ts

main.server.ts

platformBrowser()

platformServer()

app.module.ts

app.server.module.ts

DevHelp.Online

How does Universal work?

Angular App

BrowserModule

imports AppModule

imports ServerModule

app.module.ts

app.server.module.ts

DevHelp.Online

How does Universal work?

@NgModule({
  exports: [BrowserModule],
  imports: [HttpModule, HttpClientModule, NoopAnimationsModule],
  providers: [
    SERVER_RENDER_PROVIDERS,
    SERVER_HTTP_PROVIDERS,
    {provide: Testability, useValue: null},
  ],
})

export class ServerModule { } // <--

ServerModule imports all the Dependency Injection -magic-

DevHelp.Online

Static vs Dynamic Rendering?

Static prerendering is done at BUILD-time
  - Great for Static sites
  - Each route will have its content pre-rendered into an .html

     file

  - Simply `serve` up the dist/browser folder to your server.

DevHelp.Online


/ dist /

    / browser / 
    
        index.html
        about.html
        contactus.html
        // ... etc ...  

Static vs Dynamic Rendering?

Dynamic SSR is done at RUN-time
  - This is more typical, dynamic sites where data is Http based

     and data is constantly changing.
  - Each route will render at run-time when requested and is            served to the client.    

  - You must deploy the Node server

DevHelp.Online


/ dist /

    / browser /
        index.html
        // static assets
 
    / server / 
        // our server "universal" bundle, etc...

    // ** Our Node Express server we need to fire up **
    server.js

Node server time

import { AppServerModuleNgFactory } from './PATH/TO/server.bundle';
import { renderModuleFactory } from '@angular/platform-server';
import * from 'fs';

// Express server
const app = express();

app.get('*', (req, res) => {

    renderModuleFactory(AppServerModuleNgFactory, {
        extraProviders: [
            {
                provide: INITIAL_CONFIG,
                useValue: {
                  document: fs.readFileSync('dist/browser/index.html').toString(),
                  url: req.originalUrl
                }
            },
            // other providers you want to pass in
        ]
    }).then(html => {
        res.send(html);

    }).catch(error => { // errors });
});

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

This is NOT a working example - just conceptual

DevHelp.Online

For every Request

Serialize our Angular AoT'd Angular NgModule

Our index.html

Current URL

Send the resulting <html>
to the Browser!

Node server time

You can use renderModuleFactory() manually,
but I'd recommend just using the expressEngine.

http://www.github.com/angular/universal

Additional Universal libraries found in the npm namespace @nguniversal

 

More add-ons, coming soon!

DevHelp.Online

@nguniversal express-engine


import { ngExpressEngine } from '@nguniversal/express-engine';
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';

// Our Universal express-engine (github.com/angular/universal)
app.engine('html', ngExpressEngine({
    bootstrap: AppServerModuleNgFactory,
    providers: [
        provideModuleMap(LAZY_MODULE_MAP)
    ]
}));

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

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

DevHelp.Online

Notice Request/Response are passed in to the Engine which handles creating the InjectionTokens and passes them into Angular,
now you can use them within your Angular App!

import { Injector } from '@angular/core';

import { Request, Response } from 'express';
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; // <--

export SomeComponent {

    private request: Request;
    private response: Response;

    constructor(
        private injector: Injector
    ) { 
        this.request = this.injector.get(REQUEST);
        this.response = this.injector.get(RESPONSE);

        // cookies / headers / anything from our original Node Request
        // same with Response
    }
}

DevHelp.Online

Request/Response inside your Angular App

@nguniversal express-engine

Grab the Request/Response using the Injector

DevHelp.Online

Common SSR Gotchas

Server-side Render

Client-side Render

A

B

C

A

B

C

Renders entire App

Renders entire App
Again

DevHelp.Online

Common SSR Gotchas

Server-side Render

Client-side Render

A

B

C

A

B

C

Http call made here

Http call will be triggered -again-

Template "flickers" when Data goes blank then re-appears

DevHelp.Online

Common SSR Gotchas

Server-side Render

Client-side Render

A

B

C

A

B

C

Component "B" uses
"window" or "document"

"window/document" exists in the Browser-world

How does Node handle it?

Throws error, server stops.

No Bueno...

How can we handle these common SSR issues in our Angular Universal Apps?

New to Angular 5.0

- Upcoming in Angular 5.0 is the new TransferState

  - Use this so Http calls don't get hit TWICE
    Once during SSR, and again during CSR.

- Use TransferHttpCacheModule to have all GET HttpClient requests automatically transferred and re-used!

import { BrowserTransferStateModule } from '@angular/platform-browser';
import { TransferHttpCacheModule } from '@nguniversal/common';

imports: [
  // ...
  HttpClientModule,
  TransferHttpCacheModule, // <-- automatically caches HttpClient GET requests
  BrowserTransferStateModule // <--
]

DevHelp.Online

import { ServerTransferStateModule } from '@angular/platform-server';
imports: [ 
  // ... 
  ServerTransferStateModule // <--
]

Browser (app.module.ts)

Server (app.server.module.ts)

DevHelp.Online

Now with HttpTransferCache

Server-side Render

Client-side Render

A

B

C

A

B

C

Http call made here

Http call re-used

Data sent down with html and re-used
when CSR calls the same Http call

Other Universal "Gotchas"

- Never touch the "window"
     - If you must, create a WindowService and use
       Depenency Injection to provide different versions 

@Injectable()
export class WindowService {
  // Some typical window use-cases
  public navigator: any = {};
  public location: any = {};
  public alert(msg: string) { return; }
  public confirm(msg: string) { return; }
  public setTimeout(handler: (...args: any[]) => void, timeout?: number): number { return 0; }
  public clearTimeout(timeoutId: number): void { }
  public setInterval(handler: (...args: any[]) => void, ms?: number, ...args: any[]): number { return 0; }
  public clearInterval(intervalId: number): void { }
  public ga(command: string | Function, params?: any, extra?: any): void { }
  // ...
}

DevHelp.Online

export function win () {
  return typeof window !== 'undefined' ? window : {};
}

// NgModule providers[]
{ 
   provide: WindowService, 
   useFactory: (win) // ^ above
} 
// app.server.module.ts NgModule providers[]
{ provide: WindowService, useClass: WindowService }, 

Browser (app.module.ts)

Server (app.server.module.ts)

Other Universal "Gotchas"

- Never touch the "window"
     - Also, try to wrap these things and ONLY do them if you're in           the Browser Platform.              

import { OnInit, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';

export class SomeComponent implements OnInit {

  private isBrowser: boolean = isPlatformBrowser(this.platformId);

  constructor(
    private window: WindowService,
    @Inject(PLATFORM_ID) private platformId: Object
  ) { }

  ngOnInit() {

    if (this.isBrowser) { 
      (<any>this.windowService).$('body')... // something crazy involving the window
    }

  }
}

Protip: Use isPlatformBrowser() as much as possible!

DevHelp.Online

Other Universal "Gotchas"

- If you -must- use document, grab it from platform-browser


import { DOCUMENT } from '@angular/common';

constructor(@Inject(DOCUMENT) private document) { 
  this.document.querySelector('#test') // do something
}

DevHelp.Online

DevHelp.Online

Using Window/Document

Server-side Render

Client-side Render

A

B

C

A

B

C

Component "B" uses
"window" or "document"

"window/document" exists in the Browser-world

Because of Dependency Injection, window never used.

Node keeps running

Other Universal "Gotchas"

- Be careful with setTimeout & setInterval

 

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

export class SomeComponent {

  private isBrowser: boolean = isPlatformBrowser(this.platformId);

  constructor(
    private window: WindowService,
    @Inject(PLATFORM_ID) private platformId: Object
  ) { }

  ngOnInit() {
    if (this.isBrowser) { 
      setTimeout(() => { }, 1000);
    }

    // or you can use the WindowService you create
    if (this.isBrowser) { 
      (<any>this.windowService).setTimeout(() => { }, 1000);
    }
  }
}

They WILL delay your Server-render, as Zones will be waiting

DevHelp.Online

Want more?

DevHelp.Online

For more Gotchas and other tips & tricks
Visit the Universal repo:

http://www.github.com/Angular/Universal

... More documentation coming soon ...

In Conclusion

  • SSR can help you avoid the "Loading" splash screen
    • Especially helpful on slower networks
  • On slower networks (3G / etc) you can get a faster initial paint and "perception" that the App's performance is much faster.
    • Can be ~ 2-3x faster "initial paint"
  • You can finally get SEO & Social-Media Previews
  • Be mindful of browser-specific things you might be using in your code!
    • Also make sure you choose 3rd party libraries carefully as well! For the same reason.

In Conclusion

We went from

To this:

As for the question of
"can we have the best of both worlds? "

Yes!

( with a little effort of course )

So ...

Thank You !

Follow me on Twitter
I'll post a link to the slides later

Github
@MarkPieszak

 

Twitter

@MarkPieszak

Medium

@MarkPieszak
 

DevHelp.Online

http://www.DevHelp.Online
Consulting / Training / Workshops / Development