Angular v4 Universal Demystified
You can get a more up-to-date version at https://leanpub.com/angular-universal
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.tsimport { 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.tsimport '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.jsconst 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.