Angular Server Side Rendering

Angular

The new shiny Front End framework of Google, Angular, finally has been released and it is being updated actively every day. Using Angular we can develop very fast, complex web applications without rotting our code.

Angular has a great variety of tools build around it to make development very easy, Angular CLI embraces editors, helping to generated components, modules, services in a second with a simple command.

Opposed to Angular CLI we could also use Webpack for compiling and building our application. Webpack requires more experience and the configuration is not easy but it allows expert users to take control of every compilation step. The main reason I decided to write this tutorial is because I couldn’t find any complete tutorial on how to setup a server side rendering, even on the Angular.io website.

SEO vs SPA

Single Page Application have been replacing static web sites in the past years, and the trend is growing very fast with the support for Progressive Web Apps, which try to smooth the differences between a Native Application and a Web Application.

Meanwhile Native Apps have dedicated market stores (Play Store, App Store), SPA apps relies only on the search engines to be indexes. As we know crawlers have difficulties on indexing SPA, since crawlers ignore content that is served by javascript.

When SEO really matters for our application we might get in trouble, indeed many of past year SPA frameworks did not provide a good solution for SEO. Fortunately, it seems that Google has solved many of SPA problems, including SEO.

Let’s code it

Time to code and explain step by step how to run server side rendering with Angular (4+). The idea is to use node to serve our application the same way as we use the browser. Our Angular application will be a full normal server side application running on node (the same way a php scripts runs on server). Angular handles this fine as far as we do not access directly to browser DOM.

  1. We need an Angular App, you can use any webpack seed or download the skeleton I have prepared in this github repo, or simply use your own one
  2. Create another bootstrap module file for our application. Lets add app.server.module.ts file:
app.server.module.ts
import {NgModule} from '@angular/core';
import {ServerModule} from '@angular/platform-server';
import {BrowserModule} from "@angular/platform-browser";
import {AppModule} from './app.module';
import {AppComponent} from './app.component';

@NgModule({
imports: [
BrowserModule.withServerTransition({appId: 'appIdSample'}),
ServerModule,
AppModule
],
bootstrap: [AppComponent]
})
export class AppServerModule {
}

3. In your app.module.ts file, in the import section for your module add an server transition id:

src/app/app.module.ts
import {NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {FormsModule} from '@angular/forms';
import {HttpModule} from '@angular/http';
import {AppRoutingModule} from "./app.routing.module";

// App is our top level component
import {AppComponent} from './app.component';
import {HomeComponent} from './home';
import {AboutComponent} from './about';


@NgModule({
declarations: [
AppComponent,
AboutComponent,
HomeComponent
],
imports: [
BrowserModule.withServerTransition({appId: 'appIdSample'}),
FormsModule,
HttpModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent],

})
export class AppModule {

}

4. We need node web server to run the application (ex. we need apache to run php). Let create a node server based on express:

src/express-engine.ts
// angular
import { Provider, NgModuleFactory, NgModuleRef, ApplicationRef, Type } from '@angular/core';
import { platformServer, platformDynamicServer, PlatformState, INITIAL_CONFIG } from '@angular/platform-server';

// libs
import * as fs from 'fs';
import { Request, Response, Send } from 'express';

/**
* These are the allowed options for the engine
*/
export interface NgSetupOptions {
aot?: boolean;
bootstrap: Type<{}> | NgModuleFactory<{}>;
providers?: Provider[];
}

/**
* This holds a cached version of each index used.
*/
const templateCache: { [key: string]: string } = {};

/**
* This is an express engine for handling Angular Applications
*/
export function ngExpressEngine(setupOptions: NgSetupOptions): any {

setupOptions.providers = setupOptions.providers || [];

return function (filePath: string, options: { req: Request, res?: Response }, callback: Send): void {
try {
const moduleFactory = setupOptions.bootstrap;

if (!moduleFactory)
throw new Error('You must pass in a NgModule or NgModuleFactory to be bootstrapped');

const extraProviders = setupOptions.providers.concat(
getReqResProviders(options.req, options.res),
[
{
provide: INITIAL_CONFIG,
useValue: {
document: getDocument(filePath),
url: options.req.originalUrl
}
}
]);

const moduleRefPromise = setupOptions.aot ?
platformServer(extraProviders).bootstrapModuleFactory(<NgModuleFactory<{}>>moduleFactory) :
platformDynamicServer(extraProviders).bootstrapModule(<Type<{}>>moduleFactory);

moduleRefPromise.then((moduleRef: NgModuleRef<{}>) => {
handleModuleRef(moduleRef, callback);
});
} catch (e) {
callback(e);
}
};
}

function getReqResProviders(req: Request, res: Response): Provider[] {
const providers: Provider[] = [
{
provide: 'REQUEST',
useValue: req
}
];

if (res)
providers.push({
provide: 'RESPONSE',
useValue: res
});

return providers;
}

/**
* Get the document at the file path
*/
function getDocument(filePath: string): string {
return templateCache[filePath] = templateCache[filePath] || fs.readFileSync(filePath).toString();
}

/**
* Handle the request with a given NgModuleRef
*/
function handleModuleRef(moduleRef: NgModuleRef<{}>, callback: Send): void {
const state = moduleRef.injector.get(PlatformState);
const appRef = moduleRef.injector.get(ApplicationRef);

appRef.isStable
.filter((isStable: boolean) => isStable)
.first()
.subscribe(() => {

callback(state.renderToString());
moduleRef.destroy();
});
}

5. Now we need to serve our application on port (ex. apache normally will listen to port 80 to serve our php script):

src/main.server.ts
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import 'rxjs/Rx';
import {enableProdMode} from '@angular/core'
import
* as express from 'express';
import {ngExpressEngine} from './express-engine'
import
{AppServerModule} from "./app/app.server.module";

enableProdMode();

const app = express();

app.engine('html', ngExpressEngine({
bootstrap: AppServerModule
}));
app.set('view engine', 'html');
app.set('views', 'dist/server');

app.get('/', (req, res) => {
res.render('index', {req});
});

app.get('*', (req, res) => {
res.render('index', {req, res});
});

app.listen(8080, () => {
console.log('listening port 8080...')
});

main.server.ts is equivalent to main.browser.ts, but the last is served by browser, the server version is served by the ExpressEngine server. This file loads the Angular application normally (it will load all component, services, styles…) and serves it. When the browser will hit http://localhost:8080 it will get just html.

At the top of the file we import some polyfills and the AppServerModule.

6. Typescript configuration needed for compiling the server version of the Angular app:

tsconfig.webserver.json
{
"compilerOptions": {
"target": "es5",
"module": "es2015",
"moduleResolution": "node",
"declaration": false,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"pretty": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"noImplicitAny": false,
"noImplicitReturns": false,
"noImplicitUseStrict": false,
"noFallthroughCasesInSwitch": true,
"outDir": "./dist/server-build",
"lib": [
"dom",
"es6"
],
"types": [
"hammerjs",
"jasmine",
"node",
"protractor",
"selenium-webdriver",
"source-map",
"uglify-js",
"webpack"
]
},
"exclude": [
"node_modules",
"dist",
"**/*.spec.ts",
"src/main.aot.ts"
],
"angularCompilerOptions": {
"genDir": "src-server",
"skipMetadataEmit": true,
"entryModule": "./src/app/app.server.module#AppServerModule"
}
}

The main difference here is the “angularCompilerOptions” where we tell to the angular compiler the output directory and the entry module. This will be used to create the javascript version in webpack.

7. The next step is the webpack configuration, it is the same as the prod configuration with the exception that we are to output the server in a single file. We are going to pre-build a our App with the tsconfig.webserver.json file (which will create a TypeScript version), before creating the final Javascript build. (there was an edit, actually there is a second way to generate the server version explained here)

config/webpack.server.js
module.exports = {
entry: './src/main.server.ts',
output: {
path: helpers.root('dist/server'),
filename: 'server.js',
sourceMapFilename: 'server.map',
chunkFilename: 'server.[id].chunk.js'
},
}

8. Let’s run and test, add this commands in the scripts section to your package.json file:

"build:server": "npm run clean:dist && webpack --config config/webpack.server.dev.js  --progress --profile --bail",
"run:prod": "node dist/server/main.bundle.js",
"clean": "npm cache clean && npm run rimraf -- node_modules doc coverage dist compiled dll",

Now from the console run:

npm run build:server

then

npm run server:rendering

On your console now you should see:

Listening on port 8080...

9. Finally hit the browser on http://localhost:8080 and you should see your app start playing with the server.

The above example and repo works with server side. The next step is to make a normal build and point the express server to render it.

The above configuration has been extract from the Webpack-Angular-Starter repo, but has been simplified and the webpack configuration has been separated to make the workflow more fluid. The main challenged is to setup the proper compilation steps and the node server to run it. For any question please post on the github page or in the comments.

Links

Alban Xhaferllari

Ready repo https://github.com/albanx/angular-seed

Angular Introduction https://github.com/albanx/angular-tutorial