Angular v4 Universal Demystified

We will follow in this article a step-by-step procedure to be able to render our first app from the server. You can get the complete sources from this github repo at tag step-01 (git checkout step-01).

Introduction

The Universal platform has been merged into Angular 4.0. This platform aims to make it possible to render Angular app pages from a server.

The key method of this Universal platform API is renderModuleFactory from the @angular/platform-server package.

The definition of this renderModuleFactory method, found in the Angular API doc is:

renderModuleFactory(
moduleFactory: NgModuleFactory<T>,
options: PlatformOptions) : Promise<string>

And the PlatformOptions type is defined in the Angular sources by the following interface:

interface PlatformOptions {  
document?: string;
url?: string;
extraProviders?: Provider[];
}

The first argument of this method is an NgModuleFactory, that is, to make it short, a compiled Angular app. This module factory has to be built once and for all.

The second argument of this method contains information about how you want to view your app: the document element indicates the contents of the HTML file you want to embed your app into and the url element indicates the page of your app you want to render.

Creating the NgModuleFactory

We will start from a fresh Angular4 app created with @angular/cli, and add the needed @angular/platform-server package (and @angular/animations dependency):

$ ng new ng-universal-demystified
$ cd ng-universal-demystified
$ npm install --save-dev @angular/animations
$ npm install --save-dev @angular/platform-server

Generally, we bootstrap an Angular app from an AppModule, which imports the BrowserModule.

In our case, we create a new Angular module, AppServerModule, which will be used to bootstrap the app from the server. This module will import AppModule and ServerModule.

// src/app.server.module.ts
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppComponent } from './app/app.component';
import { AppModule } from './app/app.module';
@NgModule({
imports: [
ServerModule,
AppModule
],
bootstrap: [
AppComponent
]
})
export class AppServerModule {}

We also need to indicate to the AppModule that we are transitioning from a server-rendered app:

// src/app/app.module.ts
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule.withServerTransition({
appId: 'ng-universal-demystified'
})
,
[...]

At this point, we can compile our app with the ngc command:

$ node_modules/.bin/ngc

After the compilation is done, we can find several new created files ending with ngfactory.ts. These are the files defining the different NgModuleFactory of your app. We will especially look at the src/app.server.module.ngfactory.ts . It ends with the following line, which indicates that the file is declaring and exporting an AppServerModuleNgFactory of type NgModuleFactory:

export const AppServerModuleNgFactory:
import0.NgModuleFactory<import1.AppServerModule>
= new import0.NgModuleFactory<any(
AppServerModuleInjector,import1.AppServerModule
);

Calling the method renderModuleFactory

We will create a little command-line utility that will take an URL as argument and render the corresponding page of our app. Here is the code for this command:

// ./main.server.ts
import 'zone.js/dist/zone-node';
import { renderModuleFactory } from '@angular/platform-server'
import { enableProdMode } from '@angular/core'
import { AppServerModuleNgFactory } from './src/app.server.module.ngfactory'
enableProdMode();
const args = process.argv.slice(2);
if (args.length != 1) {
process.stdout.write("Usage: node dist/main.js <url>");
process.exit();
}
renderModuleFactory(AppServerModuleNgFactory, {
document: `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>NgUniversalDemystified</title>
<base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root>Loading...</app-root>
</body>
</html>`,
url: args[0]
}).then(string => process.stdout.write(string));

And here is a webpack configuration to bundle this command:

// webpack.config.js
const ngtools = require('@ngtools/webpack');
module.exports = {
entry: {
main: './main.server.ts'
},
resolve: {
extensions: ['.ts', '.js']
},
target: 'node',
output: {
path: 'dist',
filename: '[name].js'
},
plugins: [
new ngtools.AotPlugin({
tsConfigPath: './tsconfig.json',
})
],
module: {
rules: [
{
test: /\.ts$/,
loaders: ['@ngtools/webpack', 'angular2-template-loader'],
},
{
test: /\.(html|css)$/,
loader: 'raw-loader'
}
]
}
}

You will need to install the angular2-template-loader before executing webpack`:

$ npm install --save-dev angular2-template-loader
$ webpack

You can now generate the home page of your app, and see that the app works!:

$ mkdir static
$ node dist/main.js / > static/index.html
$ cat static/index.html
<!DOCTYPE html><html><head>
<meta charset="utf-8">
<title>NgUniversalFromScratch</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<style ng-transition="ng-universal-demystified"></style></head>
<body>
<app-root _nghost-c0="" ng-version="4.0.1"><h1 _ngcontent-c0="">
app works!
</h1>
</app-root>
</body></html>

You are now ready to enrich your app with different pages and create static files for these different pages.

Happy Universal Rendering!

You can now read how to Build a static website with Angular Universal.