Mark Pieszak
Angular Universal Core Team -- Co-Founder of Trilon - https://Trilon.io (previously founder of DevHelp.Online)
* Co-Founder of
Development | Consulting | Training
- We help elevate teams & projects
Angular | JavaScript | Node | NestJS | ASP.NET
* Currently located in 🌞 Florida
Find us Online at https://trilon.io
SEO ✔ | |
---|---|
Faster initial paint ✔ | |
Social Media Link Previews ✔ | |
Great user experience ✔ | |
Fully interactive ✔ |
JavaScript
Application
<code />
Same JavaScript Code running
on the Server
Same JavaScript Code running
on the Browser
(nicknamed)
&
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/PROJECT_NAME folder.
/ dist /
/ my-project /
index.html
about.html
contactus.html
Request
Response
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
/ dist /
/ my-project /
index.html
/ my-project-server /
server.js
Request
Response
What options do we have?
NodeJS
NestJS
ASP.NET Core
More about NestJS Today @ 2:40pm
Oceana Room 11
$ ng new angular-mix-demo
$ ng add @nguniversal/express-engine
--clientProject angular-mix-demo
$ npm run build:ssr && npm run serve:ssr
CLI Project Name
Run your Universal App:
Angular App
BrowserModule
imports AppModule
imports ServerModule
app.module.ts
app.server.module.ts
Essentially lots of Dependency Injection
Possible because of Angular's
Compiler & Renderer not being tied to the Browser.
@NgModule({
exports: [BrowserModule],
imports: [HttpModule, HttpClientModule, NoopAnimationsModule],
providers: [
SERVER_RENDER_PROVIDERS,
SERVER_HTTP_PROVIDERS,
{provide: Testability, useValue: null},
{provide: ViewportScroller, useClass: NullViewportScroller},
],
})
export class ServerModule { }
ServerModule
imports all the Dependency Injection -magic-
Dependency Injection Mania
Provide | Use |
---|---|
RenderFactory2 | ServerRenderFactory2 |
BrowserXHR | ServerXHR (Zone wrapped) |
DOCUMENT | Domino |
PlatformLocation | ServerPlatformLocation |
StylesHost | ServerStylesHost |
Use these instead
export const SERVER_RENDER_PROVIDERS: Provider[] = [
ServerRendererFactory2,
{
provide: RendererFactory2,
useFactory: instantiateServerRendererFactory,
deps: [ServerRendererFactory2, ɵAnimationEngine, NgZone]
},
ServerStylesHost,
{provide: SharedStylesHost, useExisting: ServerStylesHost},
{provide: EVENT_MANAGER_PLUGINS, multi: true, useClass: ServerEventManagerPlugin},
];
Server -Render- Providers
export const SERVER_HTTP_PROVIDERS: Provider[] = [
{
provide: Http,
useFactory: httpFactory,
deps: [XHRBackend, RequestOptions]
},
{
provide: BrowserXhr,
useClass: ServerXhr
},
{
provide: XSRFStrategy,
useClass: ServerXsrfStrategy
},
{ provide: XhrFactory, useClass: ServerXhr },
{
provide: HttpHandler,
useFactory: zoneWrappedInterceptingHandler,
deps: [HttpBackend, Injector]
}
];
Server -Http- Providers
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
});
});
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!
For every request (URL)
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
}
}
Request/Response inside your Angular App
Grab the Request/Response using the Injector
renderModuleFactory()
Angular Code bundles
(vendor / main)
Angular bootstraps via platformBrowser()
( In the background ...)
Node Server
Render App to a String
"<html>
<app-root></app-root>
</html>"
App Code
Angular finishes bootstrapping
Now we have our
- fully functioning -
CSR Angular Application
What about...
ReferenceError: window is not defined
at new AppComponent (/Users/markpieszak/Documents/universal-talk/angularmix/dist/angularmix-server/main.js:161:9)
at createClass (/Users/markpieszak/Documents/universal-talk/angularmix/node_modules/@angular/core/bundles/core.umd.js:9323:24)
at createDirectiveInstance (/Users/markpieszak/Documents/universal-talk/angularmix/node_modules/@angular/core/bundles/core.umd.js:9210:24)
....
ng add schematic, wire everything up correctly.
Go to run the Node server locally, and...
ReferenceError: jQuery is not defined
at new AppComponent (/Users/markpieszak/Documents/universal-talk/angularmix/dist/angularmix-server/main.js:200:9)
at createClass (/Users/markpieszak/Documents/universal-talk/angularmix/node_modules/@angular/core/bundles/core.umd.js:9323:24)
at createDirectiveInstance (/Users/markpieszak/Documents/universal-talk/angularmix/node_modules/@angular/core/bundles/core.umd.js:9210:24)
....
Be aware of anything touching Browser platform specific APIs, and globals such as"window | document | ..."
If you must use something like Window,
Create a WindowService and use Depenency Injection to provide different classes per platform.
@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 { }
// ...
}
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)
Make sure you wrap all Browser-platform specific Code
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) {
(this.windowService).$('body')... // something crazy involving the window
}
}
}
Protip: Wrap
browser-only logic to prevent errors
$ npm i -S @devhelponline/create-node-mocks
In Node, the top-level object is global,
we can use this to our advantage when doing SSR to patch it with things we need in order to prevent errors, or give us functionality we need!
import { createNodeMocks } from '@devhelponline/create-node-mocks';
import { readFileSync } from 'fs';
import { join } from 'path';
const template = readFileSync(join(DIST_FOLDER, 'ANGULAR_CLI_PROJECT_NAME', 'index.html'));
createNodeMocks(template /*, { additionalWindowMocks }, { additionalGlobalMocks } */);
server.ts
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 or Mocking
Node keeps running
Platform-wrapping will become so common in your app, it helps to make a nice wrapper Service around them (and other platform-specific work you'll be doing)
@Injectable()
export class PlatformService {
get isBrowser(): boolean {
return isPlatformBrowser(this.platformId)
}
get isServer(): boolean {
return isPlatformServer(this.platformId)
}
constructor(
@Inject(PLATFORM_ID) private platformId: Object
) { }
}
Server-side Render
Client-side Render
A
B
C
A
B
C
Renders entire App
Renders entire App
Again
Rendering
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
App -flickers-
- With Angular 5.0 came 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 // <--
]
import { ServerTransferStateModule } from '@angular/platform-server';
imports: [
// ...
ServerTransferStateModule // <--
]
Browser (app.module.ts)
Server (app.server.module.ts)
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
If you -must- use document,
grab it from @angular/common
import { Inject } from '@angular/core';
import { DOCUMENT } from '@angular/common';
constructor(@Inject(DOCUMENT) private document) {
this.document.querySelector('#test');
// do something with the DOM
}
Be careful with setTimeout & setInterval
import { PLATFORM_ID } from '@angular/core';
import { PlatformService } from '../shared/platform.service';
export class SomeComponent {
constructor(
private window: WindowService,
private platformService: PlatformService
) { }
ngOnInit() {
if (platformService.isBrowser) {
(this.windowService).setTimeout(() => { }, 1000);
}
}
}
They WILL delay your Server-render, as Zones will be waiting
Cloudinary
AWS Node
Cache everything
Most users are on 3G mobile device
Cache everything
Problem:
With even Node (Angular Universal) responses cached, how can we still responsively deliver cloudinary images?
Solution:
During the SSR - Pass down all variations of image responsive sizes within an object inside a data-attribute on each element
SSR / Initial "paint"
CSR Finished
Full SPA Angular App
Time
Images loaded almost _instantly_
Very subtle differences
SSR Version - Cons / Trade-offs
Menu isn't fully functional yet
Correct font hasn't been swapped-in yet,
we default to a web standard version.
Image carousel is the first Static image at this point
Feedback (3rd party external library) hasn't loaded yet. (We lazy load other ones as well)
We can see our app is still loading & parsing
SSR Version - Pros
Scroll functionality still available.
Site/App is usable during this time!
RouterLink translated into
<a href>, navigation still works!
Although our main "carousel" isn't working yet, our users still get a full experience and a nice background image here.
For more Gotchas, tips & tricks, and utility libraries
Visit the Universal repo:
http://www.github.com/Angular/Universal
https://tinyurl.com/david-east
Trilon Consulting
Trilon.io
Consulting | Training | Workshops | Development
By Mark Pieszak
Deep-dive presentation on Angular Universal and common server-side rendering use-cases
Angular Universal Core Team -- Co-Founder of Trilon - https://Trilon.io (previously founder of DevHelp.Online)