2019
2019
@phenomnominal
You know some Angular
You've heard of Angular Universal
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
universal.app/data/:id
<html>
<link
rel="stylesheet"
href="/static/styles.css">
<script
src="/static/script.js">
<body>
<universal-app>
<div
class="loading >
</div>
</universal-app>
</body>
</html>
rel="stylesheet"
href="/static/styles.css"
src="/static/script.js"
class="loading"
"stylesheet"
"/static/styles.css"
"/static/script.js"
"loading"
Generic application loading state
Static resources
@phenomnominal
2019
universal.app/static/script.js
universal.app/static/styles.css
() {
...
}
{
...
}
...
...
function () {
...
}
html {
...
}
@phenomnominal
2019
universal.app/api/search.json?query=hi
universal.app/api/data/:id.json
{
"id": "1234567890", ...
}
{
"results": [{
"id": 1234567890,
"content": "Hi", ...
}]
}
@phenomnominal
2019
@phenomnominal
2019
universal.app/data/:id
<html>
<link
rel="stylesheet"
href="/static/styles.css">
<script
src="/static/script.js">
<body>
<universal-app>
<ua-homepage>
<ua-search>
<ua-content>
</ua-homepage>
</universal-app>
</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
2019
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
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
Pre-rendered content for Facebook, Twitter, etc.
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
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
@phenomnominal
2019
@phenomnominal
2019
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
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
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
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
https://www.youtube.com/watch?v=_trUBHaUAR0
@phenomnominal
2019
@phenomnominal
2019
@angular/platform-browser
@angular/platform-browser-dynamic
@angular/platform-server
@angular/platform-webworker // Gone!!
@angular/platform-webworker-dynamic // Gone!!
@angular/platform-terminal // !!!
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
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 { }
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
Do not use the DOM directly! There's almost always a way to do the same thing with Angular's abstractions.
@phenomnominal
2019
@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
) { }
}
@phenomnominal
2019
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;
}
}
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;
}
}
Some third-party code won't want to play nicely...
Set up globals
Require troublesome module
Clean up after
Directly check for window
@phenomnominal
2019
declare var global: any
if (typeof window === 'undefined') {
(global).window = {};
(global).requestAnimationFrame = () => void 0;
require('../src/cursor.js');
delete (global).window;
delete (global).requestAnimationFrame;
}
As a last resort (please!)
@phenomnominal
2019
If you really must...
Injectable PLATFORM_ID token
Use the method for the specific 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();
}
}
@phenomnominal
2019
Add @Optional injection annotation
Trusty if statement
Check for the presence of an injected service
@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
@NgModule({
imports: [
// ...
],
bootstrap: [AppComponent],
providers: [
{ provide: VideoService, useValue: null }
]
})
export class AppServerModule { }
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
The whole application runs twice.
This means duplicate API calls!
This call happens twice
@phenomnominal
2019
@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
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
@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
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
@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...
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
@phenomnominal
2019
myspace/phenomnominal
geocities/phenomnomnominal