Introduction
The purpose of this library is to allow your application to support server-side rendering of your Angular 4+ applications with minimal code changes and mimimal difficulty. It supports both Angular CLI projects and projects that use custom webpack configurations. It works out of the box with @angular/material
with no hot-fixes or workarounds! It also requires zero changes to your application code: you won't have to create separate @NgModule
s, one for the server-side rendered application and one for the regular client application. You can just take your Angular code as-is and follow the steps below to get server-side rendering working.
There are two ways you can use angular-ssr
:
- If your application is an Angular CLI application with no custom webpack configuration, you can simply install it as a dependency, run a normal
ng build
, and then invoke ng-render
from node_modules/.bin
. This will result in several steps being taken:
- It will use
tsconfig.json
and some other configuration elements to compile your application to a temporary directory and load the resulting JavaScript code (application code + .ngfactory.js
files) into memory. - It will query your router configuration and collect all your application routes into a flattened array (eg.
/
, /foo
, /bar
) - For each of the discovered routes, it will instantiate your application and render that route to a static
.html
file in dist
(or, if you specified an alternate output directory using --output
, it will write the files there). It instantiates the application using the existing dist/index.html
file that was produced as part of your normal application build as a template. The pre-rendered content will be inserted into that template and written out as a new .html
file based on the route: e.g., /foo/index.html
.
- If your application has custom webpack configurations and loaders, you probably will not be able to use
ng-render
. But that's alright. It just means that you will have to build a separate webpack program output: either a NodeJS HTTP server, or a NodeJS application whose sole purpose is to do prerendering. You will follow these rough steps:
- Install
angular-ssr
as a dependency: npm install angular-ssr --save
- If you already have multiple webpack configs (one for server and one for client), then you can skip down to the next section and begin writing code to interface with
angular-ssr
. - Otherwise, you will need to add an additional output to your existing webpack configurations. This can take two forms: either you modify your existing
webpack.config.js
and just add an additional output, or you create an entirely new webpack-server.config.js
which will serve as your SSR webpack configuration. Regardless of how you accomplish it, you will ultimately need to produce two programs from webpack:
- Your normal client-side JavaScript application
- An additional server-side application that you will use to do server-side rendering. You have a couple choices here, as well:
- If you want your application to use a NodeJS application with an HTTP server inside of it that will do on-demand pre-rendering of your application routes, then do that. We can then write a few lines of code to do the actual pre-rendering / caching inside of your route handlers. It doesn't matter if you use koa or express or any other HTTP server you wish to use --
angular-ssr
will not integrate directly with the HTTP server anyway. It just exposes a very simple API to get pre-rendered HTML documents, and you can integrate this with your server in whichever way makes the most sense for your application. - Alternatively, you can build an application whose sole purpose is to do server-side rendering at build-time. This application will produce some static pre-rendered application content and then exit. This use-case makes sense if your application will not need to do on-demand server-side rendering. Let's say for example you just have an application with a few routes (
/a
, /b
, /c
, etc.). In this case, since all routes are known in advance and none of them take any URL parameters, we can just pre-render each route at build time and spit out some .html
files. - Let's say that your application does need on-demand rendering, though. For example, you are writing a blog application that has URLs like
/blog/post/1
, /blog/user/3
, etc. In this case, you will need to do on-demand server-side rendering. No problem! In this use-case, it makes sense to build a small HTTP server using express or koa and to write a few lines of code to integrate with angular-ssr
. Then from inside your server, you can demand render and cache particular routes with arguments like /blog/post/1
. I will show you some examples of how to do this below.
The simplest possible case: an Angular CLI application with no built-in HTTP server and no need for on-demand rendering
If your application was generated by ng new
and does not use any custom webpack configuration, then you will be able to use the ng-render
CLI tooll to automatically pre-render your application routes into static .html
files. It is worth emphasizing that this use case is the easiest, but also the least flexible. If you need on-demand rendering, or if you have custom webpack configurations, then you should skip down to the examples below as they will cover your use-case better than this section.
But, in the event that you do have a simple ng cli
application, you can give angular-ssr
a whirl just by doing:
npm install angular-ssr --save
ng build
./node_modules/.bin/ng-render
It should spit out some messages like:
[info] Writing rendered route / to /Users/bond/proj/dist/index.html
[info] Writing rendered route /foo to /Users/bond/proj/dist/foo/index.html
[info] Writing rendered route /bar to /Users/bond/proj/dist/bar/index.html
You can then do cd dist
and run:
npm install -g http-server
http-server .
Then when you load the application by hitting http://localhost:8080
, you should see the pre-rendered document in the initial HTTP response (for each route in your application).
On-demand server-side rendering and caching
Let's get this use-case out of the way first, because I think it is likely to be the most common usage of angular-ssr
.
You have an HTTP server application that you build as part of your application using webpack. Your HTTP server is written in TypeScript. (If your HTTP server is written in JavaScript, the library will still work in the same way, but you won't be able to copy-paste the code below.)
When you build your application, you are outputting two targets: your actual Angular client application, and your HTTP server application. We are going to focus on the server application here because there will be zero changes to your application code.
Your actual HTTP server code will look something like the following:
import 'reflect-metadata';
import {
ApplicationFromModule,
fileFromString,
routeToPath,
} from 'angular-ssr';
import {join} from 'path';
import {AppModule} from '../src/app/app.module';
const dist = join(process.cwd(), 'dist');
const templateDocument = fileFromString(join(dist, 'index.html'));
if (templateDocument.exists() === false) {
throw new Error('dist/index.html must exist because it is used as an SSR template');
}
const application = new ApplicationFromModule(AppModule);
application.templateDocument(templateDocument.content());
const prerender = async () => {
const snapshots = await renderer.prerender();
return snapshots.subscribe(
snapshot => {
app.get(snapshot.uri,
(req, res) => res.send(snapshot.renderedDocument));
}).toPromise();
};
prerender();
app.get('/blog/:postId',
async (req, res) => {
try {
const snapshot = await application.renderRoute(req.url);
res.send(snapshot.renderedDocument);
}
catch (exception) {
res.send(templateDocument.content());
}
});
Single-use server-side rendering as part of a build process
If your application does not fall into the categories described above (i.e., you do not need on-demand server-side rendering of all URLs), then perhaps your application falls into another category: single-use server-side rendering as part of the application build process.
In this case, your code will look similar to the HTTP server code above, but instead of integrating with express, you will simply use ApplicationRenderer
to pre-render all application routes and write them to static .html
files, which you can then serve with the HTTP server of your choosing. Again: this case only makes sense if you do not need on-demand rendering of all application routes.
In this case, your code will look something like this:
import 'reflect-metadata';
import {
ApplicationFromModule,
ApplicationRenderer,
HtmlOutput,
fileFromString,
pathFromString
} from 'angular-ssr';
import {join} from 'path';
import {AppModule} from '../src/app.module';
const dist = join(process.cwd(), 'dist');
const templateDocument = fileFromString(join(dist, 'index.html'));
if (templateDocument.exists() === false) {
throw new Error('Build output dist/index.html must exist prior to prerender');
}
const application = new ApplicationFromModule(ServerModule);
application.templateDocument(templateDocument.content());
const html = new HtmlOutput(pathFromString(dist));
const renderer = new ApplicationRenderer(application);
renderer.renderTo(html)
.then(() => {
console.log('Rendering complete');
})
.catch(exception => {
console.error('Failed to render', exception);
});
More complete examples
I am in the process of building out some more complete example applications over the next day or two. In the meantime, if you have questions you want answered, you can email me at cb@clbond.org
or post an issue in this repository and I would be more than happy to answer!
Christopher Bond