Check out myAngular article series with live demos and Facebook group Angular - Advanced Topics

Server Side Rendering in Angular

Torgeir "Tor" Helgevold
- JavaScript Developer and Blogger
Published: Sun May 15 2016

I this post I will show how to use Angular Universal to generate Angular applications on the server.

When I first heard about Angular Universal I thought the idea was really cool. Being able to serve a pre-generated application from the server is great – especially in applications where SEO is important.

Hybrid server side - client side rendering already exists in React, but it's great to see it in Angular as well now. Btw, I have an article about server side React here if you are interested.

Server side

So how does server side generation work in Angular?

One great benefit of Angular is that the framework is designed to be agnostic of the runtime environment. Since Angular abstracts the details of the host, the same application can be deployed to very different environments like browsers, express servers or even native devices.

As long as we avoid accessing environment specific functions, our code can be deployed to different environments without modification.

Examples of things to avoid are browser specific apis like direct DOM access since it doesn't have a counter part outside the browser. In cases where you need DOM access it's recommended to use the Renderer abstraction instead.

Getting started

Using the Angular Universal starter project is probable the easiest way to get started. The starter takes care of all the ceremony of configuring the project and gives you a pretty comprehensive sample as well.

Sitemap example

I've decided to use Universal to build a sitemap for my blog based on an Angular treeview component.

The treeview component is simple with a recursive template and some basic data binding.

@Component({ selector: 'tree-view', template: ` <ul> <li *ngFor="let node of nodes"> <h4 *ngIf="!node.showAsLink"> <h3 class="iconButton" (click)="node.toggle()"> <span>{{node.getIcon()}}</span> {{node.text}} </h3> </h4> <span *ngIf="node.showAsLink"> <a target="_blank" [href]="node.href">{{node.text}}</a> </span> <div *ngIf="node.expanded"> <tree-view [nodes]="node.children"></tree-view> </div> </li> </ul>` , directives: [TreeView] }) export class TreeView { @Input() nodes: Array<TreeNode>; }

Next I have defined a sitemap component to handle the http call to get the list of articles needed to build the treeview.

import {Component, Directive, ElementRef, Renderer, ViewChild} from '@angular/core'; import {Http} from '@angular/http'; import {TreeView} from '../tree-view/tree-view'; import {TreeNode} from '../tree-view/tree-node'; @Component({ selector: 'sitemap', directives: [TreeView], template: ` <h1>Sitemap SyntaxSuccess.com</h1> <div *ngIf="root.children.length > 0"> <tree-view [nodes]="root.children"></tree-view> </div>` }) export class Sitemap { root:TreeNode = new TreeNode('',[], null); constructor(public http: Http) {} ngOnInit() { this.http.get('http://some-url') .subscribe(res => { this.root = this.createArticleList(this.root, res.json()); }); } private createArticleList(node, children){ (children || []).forEach(child => { let newNode = new TreeNode(child.text, [], child.href); node.children.push(this.createArticleList(newNode, child.children)); return newNode; }); return node; } }

Notice how all the code I've written so far is totally standard Angular code – without any server vs browser specific code.

So how can we get this code to render on the server?

Server

I am using node with express to run my server. My code is a modified version of the server code from the universal starter project.

import 'angular2-universal/polyfills'; import * as path from 'path'; import * as express from 'express'; import * as bodyParser from 'body-parser'; // Angular 2 Universal import { provide, enableProdMode, expressEngine, REQUEST_URL, ORIGIN_URL, BASE_URL, NODE_ROUTER_PROVIDERS, NODE_HTTP_PROVIDERS, ExpressEngineConfig } from 'angular2-universal'; // Application import {Sitemap} from './app/sitemap.component'; const app = express(); const ROOT = path.join(path.resolve(__dirname, '..')); enableProdMode(); // Express View app.engine('.html', expressEngine); app.set('views', __dirname); app.set('view engine', 'html'); app.use(bodyParser.json()); function ngApp(req, res) { let baseUrl = '/'; let url = req.originalUrl || '/'; let config: ExpressEngineConfig = { directives: [ Sitemap ], platformProviders: [ provide(ORIGIN_URL, {useValue: 'http://localhost:8000'}), provide(BASE_URL, {useValue: baseUrl}), ], providers: [ provide(REQUEST_URL, {useValue: url}), NODE_ROUTER_PROVIDERS, NODE_HTTP_PROVIDERS, ], async: true, preboot: false // your top level app component selector }; res.render('index', config); } function indexFile(req, res) { res.sendFile('/index.html', {root: __dirname}); } // Serve static files app.use(express.static(ROOT, {index: false})); // Routes with html5pushstate app.use('/', ngApp); app.listen(process.env.PORT || 9001, () => { console.log('Started'); });

There is a lot going on here, but much of it might seem vaguely familiar if you have some previous experience with Express. You might also notice some familiar imports from Angular as well, including my sitemap component.

Client

Next we have to set up the client side bootstrapping.

This part is relatively similar to standard client side bootstrapping.

import 'angular2-universal/polyfills'; import {bootstrap, enableProdMode, BROWSER_ROUTER_PROVIDERS, BROWSER_HTTP_PROVIDERS} from 'angular2-universal'; import {Sitemap} from './app/sitemap.component'; enableProdMode(); bootstrap(Sitemap, [ ...BROWSER_ROUTER_PROVIDERS, ...BROWSER_HTTP_PROVIDERS ]);

The last step is index.html

<!doctype html> <html lang="en"> <head> <link href="site.css" rel="stylesheet"> </head> <body> <sitemap> ... Loading Sitemap ... </sitemap> <script src="/dist/client/bundle.js"></script> </body> </html>

Notice the script tag referring to bundle.js. This script tag is what enables us to kick of a client side angular application once the static server side page has rendered in the browser.

Running the code

I have deployed the code here in case you want to take a look.

Observations

If you go to my sitemap I recommend looking at the network tab while the page is loading. You will see the initial document served with fully generated html for the sitemap component. This is the result of generating the component on the server.

It's however important to point out that the server side generation is not a replacement for the client side application. In most cases we would need both, which means the components would actually instantiate twice. Once on the server and again when index.html renders in the browser.

That said, if we removed the client side bundle, the application would still render, but the application would be completely static. None of the interactive JavaScript based functionality would trigger.

In my sample I need the client side application to respond to user clicks to expand and collapse the category nodes.

One downside to instantiating the component twice is that any http call made during initialization would also be made twice. In my sample you can see the call to fetch the sitemap articles is made again from the browser.

I can come up with a few workarounds for short circuiting the http call. I have however decided to wait to see what the recommendations will be from the Universal team. Two workarounds I can think of are

  1. Embed json in a hidden field and pass it down to the client (essentially view state)
  2. Include a script tag from the server that leaves behind a global variable that contains the data

Both solutions would probably work, but I think this is something that should be streamlined as part of the framework.

Conclusion

Angular Universal is really cool! I see it mostly as an SEO feature, but I am curious to see if there are scenarios where it can be used to improve performance as well.

If you liked this article, share it with your friends on social media:

We also have a new Facebook group about advanced Angular topics.

I invite you to follow me on twitter