16

I am working on an Angular4 webpack project to which I wanted to add AngularUniversal to make server side rendering possible.But most tutorials are using angular cli.I want to integrate Universal with webpack.I tried following this tutorial with no luck.Can someone please help.

RemyaJ
  • 5,358
  • 4
  • 22
  • 41

4 Answers4

14

This Angular Universal is only for Angular 2. If you want to start from scratch you can use this Angular 4 Universal Seed which has all features like:

  • Angular 4
  • WebPack
  • dev/prod modes
  • SCSS compilation
  • i18n, SEO, and TSLint/codelyzer
  • lazy loading, config, cache

Or if you already had Angular 4 project running you can Integrate Universal by doing following settings in your code:

Install these packages:
npm install @angular/{common,compiler,compiler-cli,core,forms,http,platform-browser,platform-browser-dynamic,platform-server,router,animations}@latest typescript@latest --save

npm install express @types/express --save-dev

Add this in your app.module.ts file

import { BrowserModule } from '@angular/platform-browser';
BrowserModule.withServerTransition({
  appId: 'my-app-id'   // withServerTransition is available only in Angular 4
}),

Create following files

src/uni/app.server.ts

import { NgModule } from '@angular/core';
import { APP_BASE_HREF } from '@angular/common';
import { ServerModule } from '@angular/platform-server';
import { AppComponent } from '../app/app';
import { AppModule } from '../app/app.module';
import 'reflect-metadata';
import 'zone.js';
@NgModule({
  imports: [
    ServerModule,
    AppModule
  ],
  bootstrap: [
    AppComponent
  ],
  providers: [
    {provide: APP_BASE_HREF, useValue: '/'}
  ]
})
export class AppServerModule {
}


src/uni/server-uni.ts

import 'zone.js/dist/zone-node';
import 'zone.js';
import 'reflect-metadata';
import { enableProdMode } from '@angular/core';
import { AppServerModuleNgFactory } from  '../../aot/src/uni/app.server.ngfactory';
import * as express from 'express';
import { ngUniversalEngine } from './universal-engine';
enableProdMode();
const server = express();
// set our angular engine as the handler for html files, so it will be used to render them.
server.engine('html', ngUniversalEngine({
    bootstrap: [AppServerModuleNgFactory]
}));
// set default view directory
server.set('views', 'src');
// handle requests for routes in the app.  ngExpressEngine does the rendering.
server.get(['/', '/dashboard', '/heroes', '/detail/:id'], (req:any, res:any) => {
    res.render('index.html', {req});
});
// handle requests for static files
server.get(['/*.js', '/*.css'], (req:any, res:any, next:any) => {
    let fileName: string = req.originalUrl;
    console.log(fileName);
    let root = fileName.startsWith('/node_modules/') ? '.' : 'src';
    res.sendFile(fileName, { root: root }, function (err:any) {
        if (err) {
            next(err);
        }
    });
});
// start the server
server.listen(3200, () => {
    console.log('listening on port 3200...');
});

src/uni/universal-engine.ts

import * as fs from 'fs';
import { renderModuleFactory } from '@angular/platform-server';
const templateCache = {}; // cache for page templates
const outputCache = {};   // cache for rendered pages
export function ngUniversalEngine(setupOptions: any) {
  return function (filePath: string, options: { req: Request }, callback: (err: Error, html: string) => void) {
    let url: string = options.req.url;
    let html: string = outputCache[url];
    if (html) {
      // return already-built page for this url
      console.log('from cache: ' + url);
      callback(null, html);
      return;
    }
    console.log('building: ' + url);
    if (!templateCache[filePath]) {
      let file = fs.readFileSync(filePath);
      templateCache[filePath] = file.toString();
    }
    // render the page via angular platform-server
    let appModuleFactory = setupOptions.bootstrap[0];
    renderModuleFactory(appModuleFactory, {
      document: templateCache[filePath],
      url: url
    }).then(str => {
      outputCache[url] = str;
      callback(null, str);
    });
  };
}

Add below configuration in your tsconfig.ts file which I assume located in root dir

{
    "compilerOptions": {
        "baseUrl": "",
        "declaration": false,
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "lib": ["es2016", "dom"],
        "moduleResolution": "node",
        "outDir": "./dist/out-tsc",
        "sourceMap": true,
        "target": "es5",
        "module": "commonjs",
        "types": ["node"],
        "typeRoots": [
            "node_modules/@types"
        ]
    },
    "files": [
        "src/uni/app.server.ts",
        "src/uni/server-uni.ts"
    ],
    "angularCompilerOptions": {
        "genDir": "aot",
        "entryModule": "./src/app/app.module#AppModule",
        "skipMetadataEmit": true
    },
    "exclude": [
        "test.ts",
        "**/*.spec.ts"
    ]
}

Atlast your webpack.config.uni.js in root dir

const ngtools = require('@ngtools/webpack');
const webpack = require('webpack');
const path = require('path');
const ExtractTextWebpackPlugin = require("extract-text-webpack-plugin");
module.exports = {
    devtool: 'source-map',
    entry: {
        main: ['./src/uni/app.server.ts', './src/uni/server-uni.ts']
    },
    resolve: {
        extensions: ['.ts', '.js']
    },
    target: 'node',
    output: {
        path: path.join(__dirname, "dist"),
        filename: 'server.js'
    },
    plugins: [
        new ngtools.AotPlugin({
            tsConfigPath: './tsconfig.json'
        })
    ],
    module: {
        rules: [
            {
                test: /\.(scss|html|png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
                use: 'raw-loader'
            },
            { test: /\.ts$/,  loader: require.resolve('@ngtools/webpack') },
            {
                test: /\.(png|jpg|woff|woff2|eot|ttf|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
                loader: 'url?limit=512&&name=[path][name].[ext]?[hash]'
            },
            { test: /\.scss$/, use: [{
                loader: "style-loader" // creates style nodes from JS strings
            }, {
                loader: "css-loader" // translates CSS into CommonJS
            }, {
                loader: "sass-loader" // compiles Sass to CSS
            }] }
        ]
    }
}

Add below scripts in you package.json file:

"ngc-build": "ngc -p ./tsconfig.json", // To generate ngFactory file
"build:uni": "webpack --config webpack.config.uni.js",
"serve:uni": "node dist/server.js",

There are certain things that we should keep in mind:

  • window, document, navigator, and other browser types - do not exist on the server - so using them, or any library that uses them (jQuery for example) will not work. You do have some options given in this link if you truly need some of this functionality.
mohit
  • 1,878
  • 1
  • 16
  • 27
5

Angular universal is only used for angular 2.x. Angular 4.x you need to use platform server.Examples are as follows:

For angular 2.X.X :

AngularClass's seed project using express/universal

https://github.com/angular/universal-starter

For angular 4.X.X Use angular platform server

https://github.com/ng-seed/universal

There are few other examples also:

Deepak Kumar
  • 1,669
  • 3
  • 16
  • 36
3

The example that is mentioned in given tutorial is using the example mentioned in Angular Resources section. They have recently updated their docs and have not yet provided the detailed documentation to implement @angular/universal. This used to be the page you are looking for but it had some issues mentioned here. May be that's why they removed it and have decided to rewrite it.

Devesh Jadon
  • 7,004
  • 4
  • 22
  • 27
1

You can find an Angular 4 tutorial on server-side rendering with Webpack on this blog.

Features:

  • it is built upon Angular 4
  • it does not depend on Angular CLI
  • it builds upon Webpack
  • the blog provides a step by step instruction in three phases:
    • phase 1: Run a Server Side Rendered Hello World Page on a Docker Container (I am providing a pre-installed Docker image for your convenience, but the instructions should work on your own Angular environment)
    • phase 2: Create a new functional Link on the main Page
    • phase 3 (optional): Dynamically insert a WordPress Blog POST via RESTful API

The end result can be viewed easily on a Docker host like follows:

(dockerhost)$ docker run -it -p 8002:8000 oveits/angular_hello_world:centos bash
(container)# git clone https://github.com/oveits/ng-universal-demo
(container)# cd ng-universal-demo
(container)# npm i
(container)# npm run start

I have chosen port 8002 above, since I am running other examples on ports 8000 and 8001 already; If the docker host is running on Virtualbox, you might need a port mapping from 8002 of the Virtualbox host to 8002 of the Virtualbox VM.

On a browser, navigate to http://localhost:8002/blog. You will see the content of a blog post that is downloaded from the Wordpress API. With right-click->view source you will see the HTML content. This demonstrates that this is a server-side rendered page.

P.S.: Like the tutorial you have tried out, the tutorial is based on a Git project that originally has been created by Rob Wormald, but with this fork by FrozenPandaz, I have found a version that is upgraded to Angular 4 and has worked better with Webpack (see the appendix of the blog for more details).

Olli
  • 1,621
  • 15
  • 18