Server Side Rendering(SSR) in Angular 5 with Angular Universal. Angular Universal is not very easy to get it right for the first time. After going through Angular Universal documentation for Angular 5 I personally found that it is not proper, they missed mentioning some of the points related to ts-loader and modifying .angular-cli.json file etc.,
Why Angular Universal??
1. It is helpful for Search Engine Optimization(Facilitate web crawlers).
2. Improves the performance of your application on mobile and low powered devices.
3. It shows your first web page very quickly.
It is also very helpful for social media sites which rely on web crawlers to index pages, and make the content available for the search.
On the way of implementing you might get errors. To avoid, some of the things should be kept in mind.
1. Your current @angular/cli version should be in compatible with webpack version.
2. There should not be two webpacks in your current project. You can check it by running command npm ls webpack.
3. After your build was successful you might get some errors. So make sure that @angular/core version and @angular/platform-server(which we are going to install in a while) are of the same version or at least in compatible. You might find these stack overflow answers helpful.
4. Make sure even @angular/platform-browser and @angular/platform-browser-dynamic are of similar versions.
5. Make sure you have ts-loader(which we are going to install in a while) in compatible with your webpack version.
Server Side Rendering(SSR) in Angular 5 with Angular Universal
For your reference I am going to give you my package.json file here.
{ "name": "App-Name", "version": "0.0.0", "license": "MIT", "scripts": { "ng": "ng", "start": "webpack-dev-server --port=4200", "build": "webpack", "test": "karma start ./karma.conf.js", "lint": "ng lint", "e2e": "protractor ./protractor.conf.js", "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", "pree2e": "webdriver-manager update --standalone false --gecko false --quiet" }, "private": true, "dependencies": { "@angular/animations": "^5.2.0", "@angular/cdk": "^5.2.3", "@angular/common": "^5.2.0", "@angular/compiler": "^5.2.0", "@angular/core": "^5.2.0", "@angular/forms": "^5.2.0", "@angular/http": "^5.2.0", "@angular/material": "^5.2.3", "@angular/platform-browser": "^5.2.0", "@angular/platform-browser-dynamic": "^5.2.0", "@angular/platform-server": "^5.2.0", "@angular/router": "^5.2.0", "@nguniversal/express-engine": "^6.0.0", "@nguniversal/module-map-ngfactory-loader": "^6.0.0", "angularfire2": "^5.0.0-rc.11", "bootstrap": "^4.1.3", "core-js": "^2.4.1", "firebase": "^5.3.0", "jquery": "^3.3.1", "jwt-decode": "^2.2.0", "moment": "^2.22.1", "popper.js": "^1.12.9", "rxjs": "^5.5.6", "ts-loader": "^3.5.0", "zone.js": "^0.8.19" }, "devDependencies": { "@angular/cli": "~1.7.1", "@angular/compiler-cli": "^5.2.0", "@angular/language-service": "^5.2.0", "@types/jasmine": "~2.8.3", "@types/jasminewd2": "~2.0.2", "@types/node": "~6.0.60", "codelyzer": "^4.0.1", "jasmine-core": "~2.8.0", "jasmine-spec-reporter": "~4.2.1", "karma": "~2.0.0", "karma-chrome-launcher": "~2.2.0", "karma-coverage-istanbul-reporter": "^1.2.1", "karma-jasmine": "~1.1.0", "karma-jasmine-html-reporter": "^0.2.2", "protractor": "~5.1.2", "ts-node": "~4.1.0", "tslint": "~5.9.1", "typescript": "~2.5.3", "webpack-dev-server": "~2.11.0", "@angular-devkit/core": "0.3.2", "@ngtools/webpack": "1.10.2", "webpack": "~3.11.0", "autoprefixer": "^7.2.3", "file-loader": "^1.1.5", "html-webpack-plugin": "^2.29.0", "less-loader": "^4.0.5", "postcss-import": "^11.0.0", "postcss-loader": "^2.0.10", "postcss-url": "^7.1.2", "raw-loader": "^0.5.1", "sass-loader": "^6.0.6", "istanbul-instrumenter-loader": "^3.0.0", "style-loader": "^0.19.1", "stylus-loader": "^3.0.1", "url-loader": "^0.6.2", "circular-dependency-plugin": "^4.2.1", "copy-webpack-plugin": "~4.4.1", "uglifyjs-webpack-plugin": "^1.1.8" } }
Don’t pay much attention towards package.json for now we will come to know about each and every detail in a while.
With all this taken care we can achieve implementing Angular Universal easily without any setbacks.
To get started install these tools
Install these packages.
1. @angular/platform-server – Universal server-side components.
2. @nguniversal/module-map-ngfactory-loader – For handling lazy-loading in the context of a server-render.
3. @nguniversal/express-engine – An express engine for Universal applications.
4. ts-loader – To transpile the server application
You can install all of them with below command and be careful with the installed versions. If the latest versions got installed, mention all of them in package.json with proper versions and install them manually by running command npm i
npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader @nguniversal/express-engine
Server Side Rendering(SSR) in Angular 5 with Angular Universal
Modify your app.module.ts file
Find the BrowserModule in the imports array and replace it with below lines of code
BrowserModule.withServerTransition({ appId: 'Your-app-id' }),
For your reference I am giving my app.module.ts file below
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpClientModule} from '@angular/common/http'; import { AppComponent } from './app.component'; import { CONST_ROUTING } from './app.routing'; import { FormsModule,ReactiveFormsModule } from '@angular/forms'; // import { AngularFireModule } from 'angularfire2'; import { MaterialModule } from './material.module'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserTransferStateModule } from '@angular/platform-browser'; import { RouterModule } from '@angular/router'; import {HomePageModule} from './homepage/homepage.module'; import { DashboardModule } from './dashboard/dashboard.module'; @NgModule({ declarations: [ AppComponent, ], imports: [ BrowserModule.withServerTransition({ appId: 'app-id' }), CONST_ROUTING, FormsModule, ReactiveFormsModule, HttpClientModule, // AngularFireModule.initializeApp(environment.firebase), MaterialModule, BrowserAnimationsModule, RouterModule, HomePageModule, DashboardModule ], entryComponents:[], providers: [], bootstrap: [AppComponent] }) export class AppModule { }
To get the run time information on which platform your application is running, use the below lines code in
your app.component.ts file
import { HttpClient } from '@angular/common/http'; import { Router } from '@angular/router'; import { Component, OnInit, Inject, PLATFORM_ID, APP_ID } from '@angular/core'; import { isPlatformBrowser, isPlatformServer } from '@angular/common'; @Component({ selector: "app-root", templateUrl: "./app.component.html" }) export class AppComponent implements OnInit { private _language; constructor( @Inject(PLATFORM_ID) private platformId: Object, @Inject(APP_ID) private appId: string, router: Router, private _http: HttpClient) { const platform = isPlatformBrowser(platformId) ? 'in the browser' : 'on the server'; console.log(`Running ${platform} with appId=${appId}`); } ngOnInit() { } }
Basically our Angular CLI(.angular-cli.json) is going to build 2 apps one which is served from the server and another which is served from the browser.
Create app.server.module.ts file in the same directory of app.module.ts file
To run our Angular Universal application, we need a server which accepts client requests and retunrs the pages.
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'; import {ServerTransferStateModule} from '@angular/platform-server'; @NgModule({ imports: [ AppModule, ServerModule, ModuleMapLoaderModule,//helps if you have implemented Lazy Loading ServerTransferStateModule ], providers: [ // Add universal-only providers here ], bootstrap: [ AppComponent ], }) export class AppServerModule {}
Server Side Rendering(SSR) in Angular 5 with Angular Universal
Create server.ts file in the root directory of your project.
Create server.ts file in the root directory of your project where your package.json file located.
// 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}`);
});
As we have 2 modules one for the browser and another one for the server we need to create typescript configuration file for the server.
Create tsconfig.server.json file
Create tsconfig.server.json file in the same directory where your tsconfig.app.json file located.
{ "extends": "../tsconfig.json", "compilerOptions": { "outDir": "../out-tsc/app", "baseUrl": "/", "module": "commonjs", "types": [ "node" ] }, "exclude": [ "test.ts", "**/*.spec.ts" ], "angularCompilerOptions": { "entryModule": "app/app.server.module#AppServerModule" } }
Server Side Rendering(SSR) in Angular 5 with Angular Universal
Next create Universal webpack configuration file.
Create a webpack.server.config.js file in the project root directory
Create a webpack.server.config.js file in the project root directory where your package.json file located
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'), {} ) ] };
We are now almost there finishing our setup all we need to do is create our main.server.ts file and modify .angular-cli.json file.
Server Side Rendering(SSR) in Angular 5 with Angular Universal
create main.server.ts file
create main.server.ts file in the same project structure where main.ts file located with below line of code.
export { AppServerModule } from './app/app.server.module';
All it does is exports server module.
We are almost done all we need to do is modify our .angular-cli.json file. As I said earlier it is going to build 2 apps one which is served from the server and another which is served from the browser.
Modify .angular-cli.json with the following lines of code
{ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "project": { "name": "App-Name" }, "apps": [ { "root": "src", "outDir": "dist/browser", "assets": [ "assets", "FHfavicon.ico" ], "index": "index.html", "main": "main.ts", "polyfills": "polyfills.ts", "tsconfig": "tsconfig.app.json", "prefix": "app", "styles": [ "styles.css", "assets/sass/typography.scss", "../node_modules/bootstrap/dist/css/bootstrap.min.css" ], "scripts": [ "../node_modules/jquery/dist/jquery.min.js", "../node_modules/popper.js/dist/umd/popper.js", "../node_modules/bootstrap/dist/js/bootstrap.min.js" ], "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", "polyfills": "polyfills.ts", "tsconfig": "tsconfig.server.json", "prefix": "app", "styles": [ "styles.css" ], "scripts": [], "environmentSource": "environments/environment.ts", "environments": { "dev": "environments/environment.ts", "prod": "environments/environment.prod.ts" } } ], "e2e": { "protractor": { "config": "./protractor.conf.js" } }, "lint": [ { "project": "src/tsconfig.app.json", "exclude": "**/node_modules/**" }, { "project": "src/tsconfig.spec.json", "exclude": "**/node_modules/**" }, { "project": "e2e/tsconfig.e2e.json", "exclude": "**/node_modules/**" } ], "test": { "karma": { "config": "./karma.conf.js" } }, "defaults": { "styleExt": "css", "component": {} } }
This is how my .angular-cli.json looks modify yours accordingly.
Server Side Rendering(SSR) in Angular 5 with Angular Universal
Finally we need to add some scripts to our package.json file to build and serve our project files.
Already given my package.json file starting of the blog in which it is mentioned please refer back and check these scripts exists or not.
"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", "pree2e": "webdriver-manager update --standalone false --gecko false --quiet"
All we need to do now is run the command from terminal
npm run build:universal
After the successful build you can serve the files using command below
npm run serve:universal
That’s it !
Happy Coding..!
Check our latest posts in our home page Coding-Karma