Angular Prerender in 5 minutes

Angular has a prerender support once called Angular Universal which recently merged into the official @angular/platform-server package. It’s official guide for universal is still working in progress. It requires a lot of configuration if you have read the document. Most important is you need to setup and AOT configuration and use @angular/compiler-cli to compile your source code.

But if you want to know how to use this feature in a briefly way without touching too much concept like express. Read this article. I’ll show you an extreme concise example to setup a very basic prerender application.

Here we go.

Step 1

Setup a basic angular project. In this example, we have only one component and module

1
2
3
4
5
6
7
8
9
10
11
12
13
.
├── package.json
├── src
│   ├── app
│   │   ├── app.component.ts
│   │   ├── app.module.ts
│   │   └── app.server.ts
│   ├── index.html
│   ├── main.browser.aot.ts
│   ├── main.browser.ts
│   └── main.server.ts
├── tsconfig.json
├── tsconfig.aot.json

app.component.ts is a very basic component

1
2
3
4
5
6
7
8
9
import {Component} from '@angular/core';

@Component({
selector: 'app',
template: '<div>It works</div>'
})
export class App {

}

Then I make an AppModule with BrowserModule imported. Call withServerTransition will make sure prerendered app can transit into browser module.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {NgModule} from '@angular/core';
import {App} from './app.component';
import {BrowserModule} from '@angular/platform-browser';

@NgModule({
declarations: [App],
imports: [BrowserModule.withServerTransition({
appId: 'iroha'
})],
bootstrap: [App]
})
export class AppModule {

}

Step 2

To make sure server can bootstrap this app, A server module is also needed. The important part in AppServerModule is that it imports a module called ServerModule from @angular/platform-server. This module has some server replacement for the browser provider in BrowserModule.

After having these files in hand. use ngc (Angular Compiler) to compile these files, but before continue, let’s dig into the tsconfig.aot.json file to see something important for compiling with ngc.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
"compilerOptions": {
"module": "es2015",
"target": "es5",
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"noEmit": true,
"noEmitHelpers": true,
"strictNullChecks": false,
"lib": [
"es2015",
"dom"
],

"typeRoots": [
"node_modules/@types"
],

"types": [
"hammerjs",
"node"
]
}
,

"exclude": [
"node_modules",
"dist",
"src/**/*.spec.ts"
],

"awesomeTypescriptLoaderOptions": {
"forkChecker": true,
"useWebpackText": true
}
,

"angularCompilerOptions": {
"genDir": "./compiled",
"skipMetadataEmit": true
}
,

"compileOnSave": false,
"buildOnSave": false
}

To make AOT compiled code, you should use es2015 as module resolution, this will be convenient for import ngfactory and treeshaking. also their are a little options which related to angular compile. angularCompilerOptions.genDir will setup the output path for ngc, all your compiled module will be output in that path. angularCompilerOptions.skipMetadataEmit property prevents the compiler from generating metadata files with the compiled application. Metadata files are not necessary when targeting TypeScript files, so there is no reason to include them.

Then, use $(npm bin)/ngc -p tsconfig.aot.json to generate AOT files. You’ll get compiled directory in your project root. like this.

1
2
3
4
5
6
7
8
9
10
.
├── compiled
│   └── src
│   └── app
│   ├── app.component.ngfactory.ts
│   ├── app.component.ngsummary.json
│   ├── app.module.ngfactory.ts
│   ├── app.module.ngsummary.json
│   ├── app.server.ngfactory.ts
│   └── app.server.ngsummary.json

Step 3

Make some entry files for JIT browser, AOT browser and server render.

1
2
3
src/main.browser.ts,
src/main.browser.aot.ts,
src/main.server.ts

They are almost the same. but will a little different.

src/main.browser.ts

1
2
3
4
5
6
7
8
9
import 'core-js/es7/reflect';
import 'zone.js/dist/zone';
import 'ts-helpers';

import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {AppModule} from './app/app.module';


platformBrowserDynamic().bootstrapModule(AppModule);

src/main.browser.aot.ts

1
2
3
4
5
6
7
8
9
import 'core-js/es7/reflect';
import 'zone.js/dist/zone';
import 'ts-helpers';

import {platformBrowser} from '@angular/platform-browser';
import {AppModuleNgFactory} from '../compiled/src/app/app.module.ngfactory';


platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);

You may have noticed, the only difference between main.browser.ts and main.browser.aot.ts is In JIT, it use the AppModule directly and bootstrap that module using platformBrowserDynamic.bootstrapModule(), while in AOT, it use platformBrowser.bootstrapModuleFactory() to bootstrap an ngfactory from compiled file.

that’s the different. AOT compiled component and in file bootstrap, it won’t load JIT compiler, this will make the final shipped code smaller.

finally, let’ look into the main.server.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import 'core-js/es7/reflect';
import 'zone.js/dist/zone-node';
import 'ts-helpers';

import {renderModuleFactory} from '@angular/platform-server';
import {AppServerModuleNgFactory} from '../compiled/src/app/app.server.ngfactory';
import * as fs from 'fs';
import * as path from 'path';

let template = fs.readFileSync(path.join(__dirname, './index.html'));


renderModuleFactory(AppServerModuleNgFactory, {
document: template.toString(),
url: '/'
})
.then((doc: string) => {
console.log(doc);
});

This file looks different from the previous two. it import zone-node instead of zone, because zone-node can be run in nodejs, zone cannot.

Because what we need is a string of the final html page. a factory called renderModuleFactory from @angular/platform-server is used here to bootstrap our AppServerModuleNgFactory which is imported from compiled code.

At last, you can run ts-node to get the string of rendered app.

Is that over. exactly. No, Prerender also involve a transition step when app has loaded into browser and your browser module has taken over all the state from server prerendered page. But this is the mainly about webpack configuration, what you need is build a bundle and shipped with the generated html document from renderModuleFactory. and use preboot.js to make the state transition.

By reading this sort article, you now setup a up a very basic project without touch any unnecessary configuration. but to make a useful application, you still need to learn how to tame webpack and build a bundle for browser. I may write something about this in the future.