Angular 2 – Angular Universal for SSR (Server Side Rendering)

If you are building an application that you like to perform well in SEO, server side rendering is definitively something you should consider.

In this guide I’ll show you how you can put this in place for your angular application.

The idea is simple: When you first access your app you will get a initial first page rendered in the server (nice for SEO). Your browser will then download all the files necessary to run in a SAP(single page application) mode which means, you will have all the benefits or SAP applications.

How to get started

  1. Install platform-server package.
    The platform-server package has server implementations of the DOM, XMLHttpRequest, and other low-level features that do not rely on a browser.
    You will then compile your client application with the platform-server module instead of the platform-browser module (used by default) and run the resulting Universal app on a web server.

The renderModuleFactory function takes as inputs:
– an HTML template page (usually index.html)
– an Angular module containing components
– a route that determines which components to display.

The route comes from the client’s request to the server. Each request results in the appropriate view for the requested route.
The renderModuleFactory renders that view within the tag of the template, creating a finished HTML page ready to be served to the client.

Working around the browsers API in the server

Naturally a server app doesn’t execute in the browser, therefore you will need to work around some of the browser APIs and capabilities that are missing.
You won’t be able to reference browser-only native objects such as window, document, navigator or location. If you don’t need them on the server-rendered page, side-step them with conditional logic otherwise use an injectable Angular abstraction over the object you need: Location or Document are just 2 examples of these abstractions.

If angular doesn’t provide an abstraction that fits your needs you can always write your own abstractions that will delegate to the app once the client side version is rendered.

Without mouse or keyboard events, a universal app can’t rely on a user clicking a button to show a component. A universal app should determine what to render based solely on the incoming client request. This is a good argument for making the app routeable (aka use the router for navigation).

Let’s get our hands dirty

Step1: Download angular demo

We need something to get start so we’ll be using the angular demo app.

Step2: Npm install

Step3: Add a few extra module specifically for the universal render

npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader @nguniversal/express-engine

Step4: Let’s modify our client app – open src/app/app.module.ts and update it to reflect the following file:

import { APP_ID, Inject, NgModule, PLATFORM_ID} from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';

import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';

import { AppRoutingModule } from './app-routing.module';

import { AppComponent } from './app.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
import { HeroesComponent } from './heroes/heroes.component';
import { HeroSearchComponent } from './hero-search/hero-search.component';
import { MessagesComponent } from './messages/messages.component';
import {isPlatformBrowser} from '@angular/common';

@NgModule({
imports: [
/*
@replace:
BrowserModule into
BrowserModule.withServerTransition({appId: 'tour-of-heroes'})
*/
BrowserModule.withServerTransition({appId: 'tour-of-heroes'}),
FormsModule,
AppRoutingModule,
HttpClientModule,

// The HttpClientInMemoryWebApiModule module intercepts HTTP requests
// and returns simulated server responses.
// Remove it when a real server is ready to receive requests.
HttpClientInMemoryWebApiModule.forRoot(
InMemoryDataService, { dataEncapsulation: false }
)
],
declarations: [
AppComponent,
DashboardComponent,
HeroesComponent,
HeroDetailComponent,
MessagesComponent,
HeroSearchComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule {
/*
@add:
Modify AppModule to include a constructor an import @Inect , PLATFORM_ID, APP_ID, isPlatformBrowser
*/
constructor(
@Inject(PLATFORM_ID) private platformId: Object,
@Inject(APP_ID) private appId: string) {
const platform = isPlatformBrowser(platformId) ?
'in the browser' : 'on the server';
console.log(`Running ${platform} with appId=${appId}`);http://marcioreis.pt/wp-admin/profile.php
}
}

 

  1. Modify the browser build destination on angular.json

Since angular universal requires to different builds where one is the client side app and other is the server side app, we need to instruct the angular.json to put browser stuff into dist/browser as show below.

{
  "$schema": "./node_modules/@angular-devkit/core/src/workspace/workspace-schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
  "angular.io-example": {
  "root": "",
  "projectType": "application",
  "prefix": "app",
  "architect": {
  "build": {
  "builder": "@angular-devkit/build-angular:browser",
  "options": {
  "outputPath": "dist/browser", // <--- Update to this value

 

 

  1. Replace all relative urls (used to call your web service) with absolute URLs

3.1 Go to src/app/hero.service.ts and update the file as described below.

In a Universal app, HTTP URLs must be absolute, for example, https://my-server.com/api/heroes even when the Universal web server is capable of handling those requests.

constructor(
private http: HttpClient,
private messageService: MessageService) {
}

/*Your constructor will now look like this*/
constructor(
private http: HttpClient,
private messageService: MessageService,
@Optional() @Inject(APP_BASE_HREF) origin: string) {
this.heroesUrl = `${origin}${this.heroesUrl}`;
/*Note how the constructor prepends the origin (if it exists) to the heroesUrl.
You don't provide APP_BASE_HREF in the browser version, so the heroesUrl remains relative.
You can ignore APP_BASE_HREF in the browser if you've specified in the index.html
to satisfy the router's need for a base address, as the tutorial sample does.
*/
}

 

  1. Add a src/app/server.module.ts

The main.server.ts will be referenced later to add a server target to the Angular CLI configuration.

import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';

@NgModule({
imports: [
AppModule, /*Our app.module.ts*/
ServerModule, /* Our ServerModule */
ModuleMapLoaderModule /* The ModuleMapLoaderModule is a server-side module that allows lazy-loading of routes.*/
],
providers: [
// Add universal-only providers here
],
bootstrap: [ AppComponent ],
})
export class AppServerModule {}
  1. Set an app server entry point

The Angular CLI uses the AppServerModule to build the server-side bundle.
Create a main.server.ts file in the src/ directory that exports the AppServerModule:

5.1 Create src/main.server.ts

export { AppServerModule } from './app/app.server.module';

The main.server.ts will be referenced later to add a server target to the Angular CLI configuration.

  1. Universal web server

A Universal web server responds to application page requests with static HTML rendered by the Universal template engine

In this guide we will be using express as our server framework.

  1. Create a “/server.ts” in the root of our project
// These are important and needed before anything else
import 'zone.js/dist/zone-node';
import 'reflect-metadata';

import { enableProdMode } from '@angular/core';

import * as express from 'express';
import { join } from 'path';

// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

// Express server
const app = express();

const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist');

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main');

// Express Engine
import { ngExpressEngine } from '@nguniversal/express-engine';
// Import module map for lazy loading
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';

/*The ngExpressEngine is a wrapper around the universal's renderModuleFactory function that turns a client's requests into server-rendered HTML pages. You'll call that function within a template engine that's appropriate for your server stack.*/
app.engine('html',
/*The ngExpressEngine function returns a promise that resolves to the rendered page.
ngExpressEngine is just a wrapper to hide the complexity of the page render.
There are other wrappers for other languages and frameworks.
*/
ngExpressEngine({
/*The first parameter is the AppServerModule that we created earlier.*/
bootstrap: AppServerModuleNgFactory,
/*You supply extraProviders when your app needs information that can only be determined by the currently running server instance. In this case we are injecting the APP_BASE_HREF token*/
providers: [
provideModuleMap(LAZY_MODULE_MAP)
]
}));

app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));

// TODO: implement data requests securely
/*Your server will probably expose a web service that will feed your client app with data (and now server too).
Probably your web service urls will start by something like "/api/*" right?
Below you should setup your routes as you would normally do to create your web service.

app.get('/api/users', (req, res) => {
res.json([{"name":"Frank"}]);
})
*/
app.get('/api/*', (req, res) => {
res.status(404).send('data requests are not supported');
});

// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));

// All regular routes use the Universal engine
app.get('*', (req, res) => {
res.render('index', { req });
});

// Start up the Node server
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});

 

Note: This sample server is not secure! Be sure to add middleware to authenticate and authorize users just as you would for a normal Angular application server.

The required information in this case is the running server’s origin, provided under the APP_BASE_HREF token, so that the app can calculate absolute HTTP URLs.

  1. Configure Universal build

8.1 First create a src/tsconfig.server.json

Paste this configuration:

{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"baseUrl": "./",
"module": "commonjs",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
],
"angularCompilerOptions": {
"entryModule": "app/app.server.module#AppServerModule"
}
}

8.2 Now create a webpack.server.config.js
This will be used to transpile our typescript server code into javascript

8.3 Add new scripts to package.json

"scripts": {
...
"build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server",
"serve:ssr": "node dist/server",
"build:client-and-server-bundles": "ng build --prod && ng run angular.io-example:server",
"webpack:server": "webpack --config webpack.server.config.js --progress --colors"
...
}
  1. Let’s try everything:

9.1 Build your app with npm run build:ssr

  • If you have issues try to install webpack-cli
    npm install webpack-cli -D

9.2 Once the build is completed run npm run serve:ssr

You will see Node server listening on http://localhost:4000 in your console.

Now you can play around with your app and see how it behaves.
You can use network Throttling to have the time to see the server side rendered page and then the sifted client side app.

You’ll also notice that (click) will not work until the page is shifted to the client side version. The only thing that will be available is the routerLinks which are built in the server and returned as compiled links to your site.

Download the final project here

Leave a Reply

Close Menu