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
@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
@MarkPieszak
Medium
@MarkPieszak
DevHelp.Online
http://www.DevHelp.Online
Consulting / Training / Workshops / Development