Rendering JavaScript on the Server
@MarkPieszak
* 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
Mark
Pie
Zack
Szak
Check us out online!
http://DevHelp.Online
Github
@MarkPieszak
@MarkPieszak
Medium
@MarkPieszak
DevHelp.Online
http://www.DevHelp.Online
Consulting / Training / Workshops / Development
DevHelp.Online
Make sure to push
↓
to not miss any Slides !
Server-side
Rendered
Websites
(Which has improved immensely
since the following screenshots of course!)
aka: SSR
DevHelp.Online
DevHelp.Online
DevHelp.Online
DevHelp.Online
DevHelp.Online
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...
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?
Server-side
Rendered
Websites
Single Page Applications
DevHelp.Online
More or less...
DevHelp.Online
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
No Search-engine optimization (at least not easily)
DevHelp.Online
Longer initial load time
- Waiting for assets to download
- Code needs to run
DevHelp.Online
DevHelp.Online
DevHelp.Online
SEO ✔ | |
---|---|
Faster initial paint ✔ | |
Social Media Link Previews ✔ | |
Great user experience ✔ | |
Fully interactive ✔ |
DevHelp.Online
DevHelp.Online
DevHelp.Online
Server-side
Rendered
Websites
Single Page Applications
"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
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
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
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
Essentially lots of Dependency Injection
Possible because of Angular's
Compiler & Renderer not being tied to the Browser.
DevHelp.Online
Dependency Injection Mania
Provide | Use |
---|---|
RenderFactory2 | ServerRenderFactory2 |
BrowserXHR | ServerXHR (Zone wrapped) |
DOCUMENT | Domino |
PlatformLocation | ServerPlatformLocation |
StylesHost | ServerStylesHost |
DevHelp.Online
Use these instead
Angular App
main.ts
main.server.ts
platformBrowser()
platformServer()
app.module.ts
app.server.module.ts
DevHelp.Online
Angular App
BrowserModule
imports AppModule
imports ServerModule
app.module.ts
app.server.module.ts
DevHelp.Online
@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 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 ...
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
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!
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
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
Grab the Request/Response using the Injector
DevHelp.Online
Server-side Render
Client-side Render
A
B
C
A
B
C
Renders entire App
Renders entire App
Again
DevHelp.Online
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
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...
- 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
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
- 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)
- 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
- 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
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
- 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
DevHelp.Online
For more Gotchas and other tips & tricks
Visit the Universal repo:
http://www.github.com/Angular/Universal
... More documentation coming soon ...
We went from
To this:
( with a little effort of course )
So ...
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