Server-side Rendering using KoaJS and Angular Universal

Ashok Vishwakarma
Ashok Vishwakarma

Mar 01, 2019 • 4 min read

Why server-side rendering?

Our web applications become SPA (Single-page Apps) as we have started using front-end frameworks like Angular, React, Vue and etc. Which simply names there is literally only one single HTML page which is served initially to the client (Browser) and the other views are generated on the client via JavaScript. The other required resources like data, images and etc. are still being handled by request-response cycle via the browser.

There are huge benefits to develop web applications as a SPA but there are some tradeoffs and the most important one is the ability for web crawlers to crawl your web application. Most of the web crawlers don’t parse the JavaScript and while crawling your web application they only get that single HTML document which has nothing significant for them to rank your page. The SSR (Server-side Rendering) is the process to bridge the gap.

Angular Universal

A normal Angular application executes in the browser, rendering pages in the DOM in response to user actions. Angular Universal generates static application pages on the server through a process called server-side rendering (SSR). When Universal is integrated with your app, it can generate and serve those pages in response to requests from browsers. It can also pre-generate pages as HTML files that you serve later.

Which simply means that you can run your Angular application not only the browser but on the server as well and help your web application to facilitate for web crawlers, improved performance for low-end devices such as mobile by showing the first page as quickly as possible.

But the official Angular Universal does not support SSR using KoaJS out of the box and in this post, I’ll be guiding that how you can do that with very minimal changes in your existing Angular application.

Dependencies

koa

KoaJS server for node

Koa - next generation web framework for node.js

koajs.com

koa-static

Koa static file serving middleware

koajs/static

Static file server middleware. Contribute to koajs/static development by creating an account on GitHub.

github.com

@angular/platform-server

The core Angular module for the server to enable SSR (Server-side rendering)

@angular/platform-server

Angular - library for using Angular in Node.js

www.npmjs.com

@nguniversal/module-map-ngfactory-loader

This is a NgFactory Loader which uses a map of modules instead of resolving modules lazily. This is useful when executing in node because lazy loading serves no purpose.

@nguniversal/module-map-ngfactory-loader

NgFactoryLoader which uses a Map to load ngfactories without lazy loading

www.npmjs.com

ts-loader

This is the typescript loader for webpack.

ts-loader

TypeScript loader for webpack

www.npmjs.com

webpack-node-externals

Easily exclude node modules in Webpack

webpack-node-externals

Easily exclude node_modules in Webpack bundle

www.npmjs.com

webpack-cli

This is the command-line interface for webpack.

webpack-cli

CLI for webpack & friends

www.npmjs.com

Run the below commands to install all at once

npm install --save koa koa-static @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader webpack-node-externals webpack-cli

There are some required changes into your existing Angular application to support SSR using Angular Universal are mentioned here, make sure you have done that before moving to the next steps.

You can skip the Step 4: Set up a server to run Universal bundles

Creating your KoaJS server

The example you have seen in the official guide (Step 4: Set up a server to run Universal bundles) is for ExpressJS but we’ll be doing the same thing with KoaJS server.

Create a universal.ts — a middleware for KoaJS server

import { Context } from 'koa';

import { renderModuleFactory } from '@angular/platform-server';
import { enableProdMode } from '@angular/core';
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';

import { readFileSync } from 'fs';
import { join } from 'path';

const {
AppServerModuleNgFactory,
LAZY_MODULE_MAP
} = require('./dist/server/main');

const _static_reg = /.js|.css|.woff|.eot|.woff2|.ttf|.svg|.png|.jpg|.jpeg|.gif|.ico/i;

// construct the dist/browser folder path here
// where your have all the static files
// for your web application
const BROWSER_DIR = join(process.cwd(), 'dist', 'browser');

const _template = readFileSync(join(BROWSER_DIR, 'index.html'), 'utf8');

// Enable prod mode for Angular
// do check if you are running on production by ENV or etc
// if (ENV === 'production') {
// enableProdMode();
// }

export default async (ctx: Context, next: Function) => {
// Ignore all static files
// they will be handled by koa-static middleware
if(ctx.req.url.match(_static_reg)) {
return;
}

// You can setup routes to disable SSR
// for admin or account pages
// which are protected by login

// For example to disable SSR for
// all the routes starts with /admin
// are not required to be SSR as
// they are behind the login
// uncomment the below code
// if(ctx.req.url.startsWith('/admin')){
// return ctx.body = _template;
// }

// proceeding with SSR for other routes
try {
const _html = await renderModuleFactory(AppServerModuleNgFactory, {
document: _template,
url: ctx.req.url,
extraProviders: [
provideModuleMap(LAZY_MODULE_MAP)
]
});
ctx.body = _html;
} catch (error) {
console.log(error);
}
}

Create a server.ts file in your root where you have your package.json and initiate your KoaJS server.

import * as Koa from 'koa';
import * as serve from 'koa-static';

import { readFileSync } from 'fs';
import { join } from 'path';

// the middileware universal.ts
// https://gist.github.com/ashokvishwakarma/7c97578108e49fb38d06777e8319bb24
import universal from './universal';

// construct the dist/browser folder path here
// where your have all the static files
// for your web application
const BORWSER_DIR = join(process.cwd(), 'dist', 'browser');

const app: Koa = new Koa();

app
.use(serve(BORWSER_DIR, {
// important so that koa-static does not serve index.html
// as directory index
index: false,
gzip: true
}))
.use(universal);

app.listen(4000);

Create webpack.server.config.js file in your root, you have your package.json

// webpack.server.config.js

const path = require('path');
const nodeExternals = require('webpack-node-externals');
module.exports = {
target: 'node',
externals: [nodeExternals()],
devtool: 'source-map',
module: {
rules: [{
test: /\.ts?$/,
loader: 'ts-loader',
exclude: /node_modules/
}],
},
resolve: {
extensions: ['.tsx', '.ts', '.js']
},
output: {
filename: 'server.js',
path: path.resolve(__dirname, 'dist', 'server')
}
};

Creating setup scripts in package.json

"scripts": {
   "build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server",
   "serve:ssr": "node dist/server.js",
   "build:client-and-server-bundles": "ng build --prod && ng run my-project:server:production",
   "webpack:server": "webpack --config webpack.server.config.js --progress --colors",
   ... 
}

To read more about Angular Universal visit the below link

Angular

Angular is a platform for building mobile and desktop web applications. Join the community of millions of developers who build…

angular.io


Ashok Vishwakarma

Ashok Vishwakarma

Google Develover Expert — WebTechnologies and Angular | Principal Architect at Naukri.com | Entrepreneur | TechEnthusiast | Speaker