Server-Side Rendering in Angular
The technology that allows us to run our Angular applications on the server is described in the Angular docs as Angular Universal.
Angular Universal generates static application pages on the server through a process called server-side rendering (SSR).
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.
A normal Angular application executes in the browser, rendering pages in the DOM in response to user actions.
In order to improve the user experience, we want to render the pages server side and send the generated static content to the client side, this will ensure that our content is crawlable by search engines and also allow users with feature phones to consume our content.
In this article you will learn how to add Angular Universal support to any existing Angular app. Let’s get started by installing dependencies
Server Side Rendering – Install dependencies
In order to implement server side rendering we need to install some additional dependencies.
Open Terminal
enter the following commands
$ npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader @nguniversal/express-engine
The root AppModule
Open file src/app/app.module.ts
and find the BrowserModule import in the NgModule metadata.
Replace that import with this one:
BrowserModule.withServerTransition({ appId: 'your App-ID' }),
Create an app.server.module.ts
file in the src/app/
directory with the following AppServerModule code:
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,
ServerModule,
ModuleMapLoaderModule
],
providers: [
// Add universal-only providers here
],
bootstrap: [ AppComponent ],
})
export class AppServerModule {}
Create a main.server.ts
file in the src
directory with the following code:
export { AppServerModule } from './app/app.server.module';
Create a server.ts
file in the project’s root directory and add the following code:
// 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.bundle');
// Express Engine
import { ngExpressEngine } from '@nguniversal/express-engine';
// Import module map for lazy loading
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP)
]
}));
app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));
// TODO: implement data requests securely
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(join(DIST_FOLDER, 'browser', 'index.html'), { req });
});
// Start up the Node server
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});
Tip: This sample server is not secure!
Create a tsconfig.server.json
file in the /src
directory to configure TypeScript and AOT compilation of the universal app.
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"baseUrl": "./",
"module": "commonjs",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
],
"angularCompilerOptions": {
"entryModule": "app/app.server.module#AppServerModule"
}
}
Create a webpack.server.config.js
file in the project root directory with the following code.
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: { server: './server.ts' },
resolve: { extensions: ['.js', '.ts'] },
target: 'node',
// this makes sure we include node_modules and other 3rd party libraries
externals: [/(node_modules|main\..*\.js)/],
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
module: {
rules: [{ test: /\.ts$/, loader: 'ts-loader' }]
},
plugins: [
// Temporary Fix for issue: https://github.com/angular/angular/issues/11580
// for 'WARNING Critical dependency: the request of a dependency is an expression'
new webpack.ContextReplacementPlugin(
/(.+)?angular(\\|\/)core(.+)?/,
path.join(__dirname, 'src'), // location of your src
{} // a map of your routes
),
new webpack.ContextReplacementPlugin(
/(.+)?express(\\|\/)(.+)?/,
path.join(__dirname, 'src'),
{}
)
]
};
Update apps section of .angular-cli.json
"apps": [{
"root": "src",
"outDir": "dist/browser",
"assets": ["assets", "favicon.ico"],
"index": "index.html",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": ["styles.css"],
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
},
{
"platform": "server",
"root": "src",
"outDir": "dist/server",
"assets": ["assets", "favicon.ico"],
"index": "index.html",
"main": "main.server.ts",
"test": "test.ts",
"tsconfig": "tsconfig.server.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": ["styles.css"],
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
],
Build and run with universal
Now that you’ve created the TypeScript and Webpack config files, you can build and run the Angular Universal application.
First add the build and serve commands to the scripts
section of the package.json
:
"scripts": {
...
"build:universal": "npm run build:client-and-server-bundles && npm run webpack:server",
"serve:universal": "node dist/server.js",
"build:client-and-server-bundles": "ng build --prod && ng build --prod --app 1 --output-hashing=false",
"webpack:server": "webpack --config webpack.server.config.js --progress --colors",
...
}
Build Universal
From Terminal, type
npm run build:universal
Serve Universal
After building the application, start the server.
npm run serve:universal
Finally. Open a browser window to http://localhost:4000/
and access your Server-side rendered Angular app.
Conclusion
With server-side rendering, we can ensure that search engines, browsers with Javascript disabled, or browsers without Javascript can still access our site content.