As of writing the latest version is 2.1.0-RC.1, which introduced ahead of time compilation (AoT) support.
First, a bit of history regarding AngularJS. Version 1 of angular was very tightly coupled with the DOM. That means that any application you write has to be run in the browser or at least some sort of browser emulator.
The inability to run angular in any environment was one of the driving forces for the complete rewrite of the angular code base. They wanted to abstract away access to the DOM and in doing so, they opened up the possibility of running angular2 within a service worker, a node server or ever a .NET server. The shackles have been broken.
A very awesome developer, PatrickJS from AngularClass, stepped up to the plate and created angular2-universal, which is the middleware that sits between the node server and angular2.
One of the main selling points of server-side rendering is the SEO value it brings. Even though googlebot is indexing single page applications, so it’s less of a concern now-a-days, I have a different post about that. There are still other search engines to consider as well as rich media sharing on social media sites like facebook and twitter.
Another great selling point is the increased perceived performance. Since the server is sending the fully rendered html page over the wire the user sees the content and is able to interact with it much faster. Couple that with a CDN for static pages and the origin server barely gets hit, which brings down the cost of running servers.
There are a few gotchas with angular2-universal and just general best practices when it comes to angular2. The main one is, don’t access the DOM directly. Any direct references to document or window in your code will cause universal to crash or at best throw errors at you. Neither of those objects exist on node, so don’t try to use them.
However…
There are certain libraries that access the DOM and they can’t be avoided in some cases. One very popular example is hammer.js, a touch events library.
Angular2 supports this library in it’s core with HammerGesturePlugin, which is essentially an extension of angular’s event binding system. Including hammer.js does not actually crash anything, it’s once you bind a touch event to an element that it actually crashes.
<div (swipe)=”myAction()” /> // Bound touch event
Here’s the very hacky work around that PatrickJS and I came up with. There’s no promise that this will work with future releases of either angular2 or angular universal.
import { __platform_browser_private__ } from ‘@angular/platform-browser’;
__platform_browser_private__.HammerGesturesPlugin.prototype.supports = function hackySupports(eventName: string): boolean {if (!this.isCustomEvent(eventName)) {return false;}return true;};
If you absolutely have to have a reference to document or window in your code the you would have to like wrap that code in an isBrowser conditional, which can conveniently be imported from angular2-universal.
import { isBrowser } from ‘angular2-universal’;
…
if (isBrowser) {// Do the thing}
Alternatively you can provide isBrowser at the root level, app.module, and then inject it as a dependency into your components, directives, pipes and services as needed.
// app.module.tsimport { NgModule } from ‘@angular/core’;import { isBrowser } from ‘angular2-universal’;
@NgModule({providers: [{ provide: ‘isBrowser’, useValue: isBrowser }]})
// my.component.tsimport { Component, Inject } from ‘@angular/core’;
@Component({…})export class MyComponent {constructor(@Inject(‘isBrowser’) public isBrowser: boolean) { }}
The main thing to take into account is that angular2-universal has it’s own module, UniversalModule. This needs to be imported at the root level or imported and then exported from the shared.module.
There are technically 2 versions of this module. One that’s imported from angular2-universal/browser and one that’s imported from angular2-universal/node. You’ll have to create 2 different app.module entry files, app.module.browers.ts and app.module.node.ts, respectively.
// app.module.browers.tsimport { NgModule } from ‘@angular/core’;import { UniversalModule } from ‘angular2-universal/browser’;
@NgModule({import: [UniversalModule]})
The node version of that file is exactly the same except that angular2-universal/browser is replace with angular2-universal/node.
UniversalModule already exports JsonpModule, HttpModule and BrowserModule. So those can safely be removed the app.module imports.
In order for angular2-universal to work with express the angular2-express-engine package needs to be installed. This is the middleware that sits between universal and express.
The route which serves the bare html file is no longer needed and can be removed from the server config. The other routes, like assets, can be kept intact. Here is a basic skeleton of what the server file looks like, it’ll all be explained below.
import { createEngine } from ‘angular2-express-engine’;import { AppModule } from ‘./app/app.module.node’;
// Express Viewapp.engine(‘.html’, createEngine({ngModule: AppModule,precompile: true}));
app.set(‘views’, ‘./dist/client’);app.set(‘view engine’, ‘html’);
app.use(‘/*’, (req, res) => {res.render(‘index’, {req,res,preboot: false,baseUrl: ‘/’,requestUrl: req.originalUrl});});
There are a few key things that are going on here.
First, the render engine needs to be set, that’s the app.engine part. createEngine takes in an object with 2 keys, ngModule and precompile. The former is the node version of the app.module and the later is a flag that tells universal if the application is in just in time or ahead of time compilation mode. Setting precompile to true means that the application is using just in time.
The app.use with ’/*’ as the first parameter In order for universal to bootstrap and render each route of the application
Then you need to make sure that express actually serves up the prerendered pages for all your routes. Make sure to add this below any other routes that you have defined in express. If you don’t, these routes will take precedent and overwrite the ones below it.
That just about covers the basics of setting up universal on a node/express server. There are a few things that I didn’t go over in this post like setting up a cache between the server and client. If your app depends on xhr requests then both the server and client will be making those requests on page load and refresh. This can be avoided by caching the response from the server and reading it on the client side.
The other pain point is setting up ahead of time compilation with universal. That’s a whole different post altogether, which is next on my Bee Todo List
Hacker Noon is how hackers start their afternoons. We’re a part of the @AMIfamily. We are now accepting submissions and happy to discuss advertising &sponsorship opportunities.
To learn more, read our about page, like/message us on Facebook, or simply, tweet/DM @HackerNoon.
If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!