Angular Universal

Angular Universal also known as server-side rendering is tool which allows server to pre-render Angular application while user hits your website for first time.

Server-side rendering has bunch of benefits for SEO, performance, and accesibility of your web-app!

In this guide we'll show you, how to implement Angular Universal working with MDB Angular and Express.js server!

Step 1: Install server dependencies

We have to install few additional dependencies for use Angular Universal. In your console type:

npm install @angular/platform-server express --save
npm install ts-loader webpack-node-externals npm-run-all --save-dev


Step 2: Create ServerModule file

We have to create app.server.module.ts file in this path: src/app/app.server.module.ts, and paste there code:


import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';

import { AppModule } from './app.module';
import { AppComponent } from './app.component';

@NgModule({
    imports: [
        AppModule,
        ServerModule
    ],
    bootstrap: [AppComponent],
})
export class AppServerModule { }


Step 3: Update app.module.file

Head into src/app/app.module.ts file, and add to imports table below code:


imports: [
    ...
    BrowserModule.withServerTransition({ appId: 'place-here-your-app-name' }),
    ...
  ],


Step 4: Create main.server.ts file

In src directory create new file called main.server.ts and put there below code:


export { AppServerModule } from './app/app.server.module';


Step 5: Update your angular-cli.json file

In src directory create new file called main.server.ts and put there below code:


{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "project": {
    "name": "pro523"
  },
  "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": [
        "../node_modules/font-awesome/scss/font-awesome.scss",
        "../node_modules/ng-mdb-pro/assets/scss/bootstrap/bootstrap.scss",
        "../node_modules/ng-mdb-pro/assets/scss/mdb.scss",
        "./styles.scss"
      ],

      "scripts": [
        "../node_modules/chart.js/dist/Chart.js",
        "../node_modules/easy-pie-chart/dist/easypiechart.js",
        "../node_modules/screenfull/dist/screenfull.js",
        "../node_modules/hammerjs/hammer.min.js"
      ],
      "environmentSource": "environments/environment.ts",
      "environments": {
        "dev": "environments/environment.ts",
        "prod": "environments/environment.prod.ts"
      }
    },
    {
      "platform": "server",
      "root": "src",
      "outDir": "dist/server",
      "assets": [],
      "index": "index.html",
      "main": "main.server.ts",
      "test": "test.ts",
      "tsconfig": "tsconfig.server.json",
      "testTsconfig": "tsconfig.spec.json",
      "prefix": "app",
      "styles": [
        "../node_modules/font-awesome/scss/font-awesome.scss",
        "../node_modules/ng-mdb-pro/assets/scss/bootstrap/bootstrap.scss",
        "../node_modules/ng-mdb-pro/assets/scss/mdb.scss",
        "./styles.scss"
      ],

      "scripts": [
        "../node_modules/chart.js/dist/Chart.js",
        "../node_modules/easy-pie-chart/dist/easypiechart.js",
        "../node_modules/screenfull/dist/screenfull.js",
        "../node_modules/hammerjs/hammer.min.js"
      ],
      "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": "scss",
    "component": {}
  }
}


Step 6: Create tsconfig.server.json file

In src directory create new file called tsconfig.server.json and put there below code:


{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/app",
    "baseUrl": "./",
    "module": "commonjs",
    "types": []
  },
  "exclude": [
    "test.ts",
    "**/*.spec.ts",
    "server.ts"
  ],
  "include": ["node_modules/ng-mdb-pro/**/*.ts",  "/**/*.ts"],
  "angularCompilerOptions": {
    "entryModule": "app/app.server.module#AppServerModule"
  }
}


Step 7: Update tsconfig.app.json file

Head into your src/tsconfig.app.json file, and update it with code below:


{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/app",
    "baseUrl": "./",
    "module": "es2015",
    "types": []
  },
  "exclude": [
    "test.ts",
    "**/*.spec.ts",
    "server.ts"
  ]
}


Step 8: Check if application build correctly

Type below commands into your terminal to check if your application builds correctly


ng build -prod --build-optimizer --app 0
ng build --aot --app 1


Step 9: Create server.ts file to use Express.js

Head into src directory, and create there server.ts file, and fill it with code below:


import 'reflect-metadata';
import 'zone.js/dist/zone-node';
import { renderModuleFactory } from '@angular/platform-server';
import { enableProdMode } from '@angular/core';
import * as express from 'express';
import { join } from 'path';
import { readFileSync } from 'fs';

enableProdMode();
const domino = require('domino');
const DIST_FOLDER = join(process.cwd(), 'dist');
const templatee = readFileSync(join(DIST_FOLDER, 'browser', 'index.html')).toString();
const win = domino.createWindow(templatee);
global['window'] = win;
global['document'] = win.document;

const PORT = process.env.PORT || 4200;

const app = express();

const template = readFileSync(join(DIST_FOLDER, 'browser', 'index.html')).toString();
const { AppServerModuleNgFactory } = require('main.server');

app.engine('html', (_, options, callback) => {
    const opts = { document: template, url: options.req.url };

    renderModuleFactory(AppServerModuleNgFactory, opts)
        .then(html => callback(null, html));
});

app.set('view engine', 'html');
app.set('views', 'src');

app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));

app.get('*', (req, res) => {
    res.render('index', { req });
});

app.listen(PORT, () => {
    console.log(`listening on http://localhost:${PORT}!`);
});


Step 10: Create webpack.config.js file

In root (main) directory of your application create file called webpack.config.js, and fill it with code below:


const path = require('path');
var nodeExternals = require('webpack-node-externals');

module.exports = {
  entry: {
    server: './src/server.ts'
  },
  resolve: {
    extensions: ['.ts', '.js'],
    alias: {
      'main.server': path.join(__dirname, 'dist', 'server', 'main.bundle.js')
    }
  },
  target: 'node',
  externals: [nodeExternals({
    whitelist: [
        /^@agm\/core/,
        /^hammerjs/
      ]
  })],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js'
  },
  module: {
    rules: [
      { test: /\.ts$/, loader: 'ts-loader' }
    ]
  }
}


Step 11: Add build scripts to package.json file

Head into your package.json file, and put there build scripts:


{
  "name": "pro523",
  "version": "0.0.0",
  "license": "MIT",
  "scripts": {
    "ng": "ng",
    "start": "ng serve",
    "build": "run-s build:client build:aot build:server",
    "build:client": "ng build -prod --build-optimizer --app 0",
    "build:aot": "ng build --aot --app 1",
    "build:server": "webpack -p",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "^5.2.0",
    "@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/platform-browser": "^5.2.0",
    "@angular/platform-browser-dynamic": "^5.2.0",
    "@angular/platform-server": "^5.2.9",
    "@angular/router": "^5.2.0",
    "chart.js": "^2.5.0",
    "core-js": "^2.4.1",
    "easy-pie-chart": "^2.1.7",
    "express": "^4.16.3",
    "font-awesome": "^4.7.0",
    "hammerjs": "^2.0.8",
    "ng-mdb-pro": "git+https://<your-token>@git.mdbootstrap.com/mdb/angular/ng-pro.git",
    "rxjs": "^5.5.6",
    "screenfull": "^3.3.0",
    "zone.js": "^0.8.19"
  },
  "devDependencies": {
    "@angular/cli": "1.6.5",
    "@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-cli": "~1.0.1",
    "karma-coverage-istanbul-reporter": "^1.2.1",
    "karma-jasmine": "~1.1.0",
    "karma-jasmine-html-reporter": "^0.2.2",
    "npm-run-all": "^4.1.2",
    "protractor": "~5.1.2",
    "ts-loader": "^3.5.0",
    "ts-node": "~4.1.0",
    "tslint": "~5.9.1",
    "typescript": "~2.5.3",
    "webpack-node-externals": "^1.6.0"
  }
}


Step 12: Update your src/main.ts file

Head into src directory, and update main.ts file:


import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

document.addEventListener('DOMContentLoaded', () => {
  platformBrowserDynamic().bootstrapModule(AppModule)
    .catch(err => console.log(err));
});


Step 13: Explore your application!

Into your terminal put last commands, and explore your Angular Universal application!


npm run build && node dist/server.js