Server Side Rendering(SSR) in Angular 5 with Angular Universal

0
6852
views

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.

Server Side Rendering(SSR) in Angular 5 with Angular Universal

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

Server Side Rendering(SSR) in Angular 5 with Angular Universal