Top shelf web developer training.

Guided Paths

Follow our crafted routes to reach your goals.

Video Courses

Premium videos to build real apps.

Written Tutorials

Code to follow and learn from.

Find Your
Opportunity HIRED
Dismiss
Up

Server-Side Rendering in Angular 2 with Angular Universal

Render your applications on the server for many benefits.

Related Course

Getting Started with Angular 2

Angular 2 is the shiny new framework that comes with a lot of new concepts. Learn all the great new features.

Server-side rendering is a headache and if you ever worked with Angular 1, you should be worried about how Angular 2 plans to handle it. Server-side rendering in Angular 2 is often-times also called Universal.

You might often hear people say stuff like: "I am building an app that will be universal". What does that even mean?

What is Universal?

When we build apps, we consider the following elements:

  1. Content
  2. Style
  3. State

By default, front end frameworks have virtual DOM so when a hard request is made to an app built with such frameworks (like Angular), the expected behavior is weird. The request is served with a response of just the HTML content found at the entry point of the app while the main app content is lost.

This is not a problem to your users because your app virtually has content, styles, and state. Where the problem kicks in is with web crawlers that index your site for SEO sake. They get served with the incomplete HTML content which is very useless to them. This behavior even gets more painful when you expect people to share your app's link and you see an image like the one below when they do:

Angular 2 Server-Side Rendering Preview

In Angular 1, if you tried to bind the app's title and description values using $rootScope you end up disappointed at how crawlers and social media robots see your site.

To replicate this challenge in Angular 2, I built an Angular 2 app with three routes (home, about and contact):

Angular 2 Server-Side Rendering Preview

Preview

Angular 2 Server-Side Rendering Source

Source

<!-- 
This all you get as content 
without server rendering
-->
<app-root>Loading...</app-root>

The conventional server rendering solution has saved us for years with Angular 1 by provisioning web crawlers with our actual content. That seemed to keep us happy but we were missing one thing -- state.

The Universal idea is to build an app that does not just render to server but also runs on the server. Running in the sense that our state, content and styles are intact on the client and the server as well. In Angular 2, this is achieved with the help of Angular Universal which loads our app on the server first, and then drops it to the browser once ready.

What is Angular Universal?

Angular 2 Universal

Angular Universal is the library the awesome Angular team is working on to make building universal apps a smooth experience. The library fixes a lot of nightmare we had working with Angular 1. As a matter of fact, our worst nightmare in Angular 1 is what Angular Universal takes away in Angular 2:

SEO Friendly

Angular Universal helps you to serve your app content to the server just as you did on the browser. When a web crawlers visit, they will be able to index you website's full content that can be used on search engines. This thereby solves one of the most popular front end challenges when working with JavaScript frameworks that create virtual DOM.

Social Media Friendly

With Angular Universal serving your sever with browser content, social media platforms that display brief information about your website when a link is added can get access to the needed details.

Pre-rendering

This is the most bind-blowing feature offered by Angular Universal. Angular Universal renders both your content and state as well as registering events on the server. The implication is that when your app boots, the server would serve the server-rendered content and a user can make use of the app at that stage.

The user's activities are recorded (with Preboot.js) at this stage and after couple of seconds when Angular is ready with the real thing, the user is automatically switched and those events replayed. The user won't even catch a glimpse of what is happening under the hood.

Getting Started

Now that we have seen what Universal and Angular Universal are, how do we join the party? Angular Universal is coming to Angular CLI real soon but before that we have projects to build now.

Universal Starter Project

Patrick built an awesome starter for Angular Universal apps which we are going to clone so as to test out these awesome tool.

# Clone repo
git clone https://github.com/angular/universal-starter scotchiversal

#Install modules
cd scotchiversal & npm i

If you have been building Angular 2 app lately, you will see that the directory looks much alike to what we are used to. The key difference is the bootstrapping process. Bootstrapping Universal apps are done with a different library and in two different files (client.ts & server.ts).

Furthermore, we usually make use of platformBrowserDynamic to bootstrap but for Universal, we use platformUniversalLibrary from the Angular Universal library:

// ./src/client.ts
// the polyfills must be the first thing imported
import 'angular2-universal-polyfills';

// Angular 2
import { enableProdMode } from '@angular/core';
import { platformUniversalDynamic } from 'angular2-universal/browser';
import { bootloader } from '@angularclass/bootloader';

import { MainModule } from './browser.module';

export const platformRef = platformUniversalDynamic();

export function main() {
  return platformRef.bootstrapModule(MainModule);
}
// Bootstrap
bootloader(main);

The bootloader function just checks that the DOM is ready before bootstrapping Angular 2.

The above logic is what we are used to but this time we are making use if platformUniversalDynamic to bootstrap. The module we are bootstrapping is imported from ./src/browser.module.ts:

// ./src/browser.module.ts
import { NgModule } from '@angular/core';
import { UniversalModule } from 'angular2-universal/browser';

@NgModule({
    ...
  imports: [
    // Import this first
    UniversalModule,
    ...
  ]
})
export class MainModule {}

Same old module that we are comfortable with. We also import the UniversalModule which must be imported before every other imports.

The two files we just looked at handles browser rendering, so let's look at server.ts and it's accompanying module for server rendering:

// ./src/server.ts
import 'angular2-universal-polyfills';

import * as path from 'path';
import * as express from 'express';
import * as bodyParser from 'body-parser';
import * as cookieParser from 'cookie-parser';
import * as morgan from 'morgan';
import * as compression from 'compression';

// Angular 2
import { enableProdMode } from '@angular/core';
// Angular 2 Universal
import { createEngine } from 'angular2-express-engine';

// App
import { MainModule } from './node.module';

// enable prod for faster renders
enableProdMode();

const app = express();
const ROOT = path.join(path.resolve(__dirname, '..'));

// Express View
app.engine('.html', createEngine({
  ngModule: MainModule,
  providers: [
      // You can include static providers like the Title service here
  ]
}));
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname);
app.set('view engine', 'html');
app.set('json spaces', 2);

app.use(cookieParser('Angular 2 Universal'));
app.use(bodyParser.json());
app.use(compression());

app.use(morgan('dev'));

function cacheControl(req, res, next) {
  // instruct browser to revalidate in 60 seconds
  res.header('Cache-Control', 'max-age=60');
  next();
}
// Serve static files
app.use('/assets', cacheControl, express.static(path.join(__dirname, 'assets'), {maxAge: 30}));
app.use(cacheControl, express.static(path.join(ROOT, 'dist/client'), {index: false}));

function ngApp(req, res) {
  res.render('index', {
    req,
    res,
    preboot: false, // turn on if using preboot
    baseUrl: '/',
    requestUrl: req.originalUrl,
    originUrl: `http://localhost:${ app.get('port') }`
  });
}
// Our page routes
export const routes: string[] = [
  'about',
  'home',
  'contact'
];
app.get('/', ngApp);
routes.forEach(route => {
  app.get(`/${route}`, ngApp);
  // Route pattern
  app.get(`/${route}/*`, ngApp);
});

app.get('*', function(req, res) {
  res.setHeader('Content-Type', 'application/json');
  var pojo = { status: 404, message: 'No Content' };
  var json = JSON.stringify(pojo, null, 2);
  res.status(404).send(json);
});

// Server
let server = app.listen(app.get('port'), () => {
  console.log(`Listening on: http://localhost:${server.address().port}`);
});

Universal is supported in Node and ASP.Net but coming to other backend platforms soon. We are using Node and to be precise to express server. The server uses the createEngine method to create a view engine that will deliver our Angular app content. The routes for each of the pages we need to render are handled as well. The rest of the code preps the express server.

MainModule is imported here too and used to render our views:

// ./src/node.module.ts
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { UniversalModule } from 'angular2-universal/node';

@NgModule({
  imports: [
    UniversalModule,
    FormsModule,
    RouterModule,
    ...
   ],
   ...
})
export class MainModule {}

Custom Components

Every other thing in the app anatomy can now be all about your custom logic. Let's create our page components:

Home Component

// ./src/app/home/home.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent {}

Home Template

<!-- ./src/app/home/home.component.html -->
<div class="jumbotron">
  <h2 class="text-center">Home</h2>
</div>
<div class="container">
  <p>. . . </p>
  <p>. . . </p>
  <div class="panel panel-default">
    <div class="panel-heading">Panel heading without title</div>
    <div class="panel-body">
      Panel content
    </div>
  </div>
</div>

Same idea applies to about and contact component so you can have fun by creating those ones yourself.

Routing

We can now create the 3 routes we specified in the server file:

// ./src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';

import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';

@NgModule({
  imports: [
    RouterModule.forChild([
      { path: '', redirectTo: '/home', pathMatch: 'full' },
      { path: 'home', component: HomeComponent },
      { path: 'about', component: AboutComponent },
      { path: 'contact', component: ContactComponent }
    ])
  ],
})
export class AppRoutingModule { }

The routes should be imported into our app module before it can work:

// ./src/app/app.module.ts
import { NgModule } from '@angular/core';

import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';

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

@NgModule({
  declarations: [ 
    AppComponent, 
    HomeComponent,
    AboutComponent,
    ContactComponent 
  ],
  imports: [
    SharedModule,
    AppRoutingModule
  ]
})
export class AppModule {}

Remember to specify the outlet for the route as well (it is easy to forget):

<!-- ./src/app/app.component.html -->
<nav class="navbar navbar-default">
  <div class="container-fluid">
    <div class="navbar-header">
      <a class="navbar-brand" href="#">Scotchiversal</a>
    </div>

    <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
      <ul class="nav navbar-nav">
        <!-- Links -->
        <li><a [routerLink]="['home']">Home</a></li>
        <li><a [routerLink]="['about']">About</a></li>
        <li><a [routerLink]="['contact']">Contact</a></li>
      </ul>
    </div>
  </div>
</nav>
<div>
  <!-- Outlet -->
  <router-outlet></router-outlet>
</div>

You can run the app with:

npm start

Angular 2 Server-side Rendering Progress

Preview

Angular 2 Server-side Rendering Progress

Source

You can see the difference in the server-served content. It clearly represents the Contact page with the form and button.

Stay Away From The DOM

The power of Universal comes from the fact that Angular abstracts DOM rendering. This is what makes Angular support for multi-platform possible. If you want Universal to keep working as expected, the you have to stay away from the DOM.

This does not mean that you cannot perform DOM operations but do not do that with the native solutions (document.domMethod() or $('dom-element')).

Angular provides us a better way to perform DOM operations safely. See ElementRef, Renderer and ViewContainer APIs for more details.

Thank you!

Chris Nwamba

JavaScript Preacher. Building the web with the JS community.