Setting up angular2-universal on your node server

This article was originally posted by Vitaliy on his blog.

 

As of writing the latest version is 2.1.0-RC.1, which introduced ahead of time compilation (AoT) support.

What is angular2-universal?

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.

Why should I care?

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.

Setting up the front-end

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 [email protected]/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.ts
import { NgModule } from [email protected]/core’;
import { isBrowser } from ‘angular2-universal’;
@NgModule({
  providers: [
    { provide: ‘isBrowser’, useValue: isBrowser }
  ]
})
// my.component.ts
import { Component, Inject } from [email protected]/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.ts
import { NgModule } from [email protected]/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.

Setting up the back-end

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 View
app.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.

Conclusion

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 @todo() list.


Leave a Reply

Your email address will not be published. Required fields are marked *