7
votes

I have an Angular app and I'm trying server-side rendering using Angular universal (https://angular.io/guide/universal) and it doesn't seem to work. I bundle my app and run it through express, I hit http://localhost:4000 and it keeps loading until I see an ERR_EMPTY_RESPONSE from the browser

I've tried pretty much everything but no luck! Any help would be much appreciated

here are the details of my code

package.json

{
  "name": "my-app",
  "version": "3.0.0",
  "author": "N/A",
  "description": "N/A",
  "scripts": {
    "ng": "ng",
    "start": "ng serve --port 8000 --host 0.0.0.0",
    "build": "ng build",
    "build:ssr": "npm run build:client-and-server-bundles && npm run webpack:server",
    "serve:ssr": "node dist/server.js",
    "build:client-and-server-bundles": "ng build --prod && ng run my-app:server",
    "webpack:server": "webpack --config webpack.server.config.js --progress --colors",
    "test": "ng test",
    "lint": "tslint ./src/**/*.ts -t verbose",
    "e2e": "ng e2e"
  },
  "private": true,
  "dependencies": {
    "@agm/core": "^1.0.0-beta.5",
    "@angular/animations": "^7.2.0",
    "@angular/common": "^7.2.0",
    "@angular/compiler": "^7.2.0",
    "@angular/core": "^7.2.0",
    "@angular/forms": "^7.2.0",
    "@angular/http": "^7.2.0",
    "@angular/platform-browser": "^7.2.0",
    "@angular/platform-browser-dynamic": "^7.2.0",
    "@angular/router": "^7.2.0",
    "@angular/upgrade": "^7.0.0",
    "@nguniversal/common": "^6.0.0",
    "@nguniversal/express-engine": "^7.0.0",
    "@nguniversal/module-map-ngfactory-loader": "^7.1.0",
    "@types/jquery": "^3.3.28",
    "@types/swiper": "^4.4.1",
    "angular-in-memory-web-api": "^0.6.0",
    "angular2-text-mask": "^9.0.0",
    "bootstrap": "^4.1.3",
    "core-js": "^2.5.4",
    "express": "^4.16.4",
    "fullcalendar": "^3.10.0",
    "moment": "^2.23.0",
    "ng-fullcalendar": "^1.7.1",
    "ngx-google-places-autocomplete": "^2.0.3",
    "ngx-infinite-scroll": "^7.0.1",
    "ngx-slick": "^0.2.1",
    "reflect-metadata": "^0.1.10",
    "replace-in-file": "^3.4.3",
    "rxjs": "^6.3.3",
    "swiper": "^4.4.6",
    "zone.js": "^0.8.27"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "~0.12.1",
    "@angular/cli": "~7.2.1",
    "@angular/compiler-cli": "^7.2.0",
    "@angular/language-service": "^7.2.0",
    "@angular/platform-server": "^7.2.4",
    "@compodoc/compodoc": "^1.1.7",
    "@types/jasmine": "~3.3.4",
    "@types/jasminewd2": "~2.0.6",
    "@types/node": "~10.12.17",
    "codelyzer": "~4.5.0",
    "jasmine-core": "~3.3.0",
    "jasmine-spec-reporter": "~4.2.1",
    "karma": "^3.1.4",
    "karma-chrome-launcher": "~2.2.0",
    "karma-coverage-istanbul-reporter": "~2.0.4",
    "karma-jasmine": "~2.0.1",
    "karma-jasmine-html-reporter": "^1.4.0",
    "protractor": "~5.4.1",
    "ts-node": "~7.0.1",
    "tslint": "^5.12.1",
    "typescript": "~3.2.2",
    "webpack-cli": "^3.2.3",
    "karma-phantomjs-launcher": "^1.0.2",
    "lodash": "^4.16.2",
    "phantomjs-prebuilt": "^2.1.7",
    "ts-loader": "^4.5.0"
  }
}

src/app/app.module.ts

import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { CommonModule } from '@angular/common';
import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';
import { TransferHttpCacheModule } from '@nguniversal/common';

// Modules
import { CoreModule } from './core/core.module';
import { SharedModule } from './shared/shared.module';
import { ConfigModule } from './configs/config.module';

// Routing
import { AppRoutingModule } from './app-routing.module';

// Components
import { AppComponent } from './app.component';


@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule.withServerTransition({appId: 'my-app'}),
    BrowserTransferStateModule,
    TransferHttpCacheModule,
    CommonModule,
    AppRoutingModule,
    HttpClientModule,
    CoreModule,
    SharedModule,
    ConfigModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

src/app/app.server.module.ts

import { NgModule } from '@angular/core';
import { ServerModule, ServerTransferStateModule } 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,
    ServerTransferStateModule
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppServerModule {}

main.server.ts

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

tsconfig.server.json

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "module": "commonjs",
    "types": []
  },
  "exclude": [
    "test.ts",
    "**/*.spec.ts"
  ],
  "angularCompilerOptions": {
    "entryModule": "app/app.server.module#AppServerModule"
  }
}

angular.json

...
     "server" : {
          "builder": "@angular-devkit/build-angular:server",
          "options": {
            "outputPath": "dist/server",
            "main": "src/main.server.ts",
            "tsConfig": "src/tsconfig.server.json",
            "fileReplacements": [
              {
                "replace": "src/environments/environment.ts",
                "with": "src/environments/environment.prod.ts"
              }
            ],
            "optimization": true,
            "sourceMap": false
          }
        }
...

After these changes, I was able to bundle the browser, server distributions successfully using ng build --prod && ng run my-app:server

here's my server.ts

// These are important and needed before anything else
import 'zone.js/dist/zone-node';
import 'reflect-metadata';

import { enableProdMode } from '@angular/core';
import { ngExpressEngine } from '@nguniversal/express-engine';

import * as express from 'express';
import { join } from 'path';

const DIST_FOLDER = join(process.cwd(), 'dist');

// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();

const domino = require('domino');
const fs = require('fs');
const path = require('path');
const template = fs.readFileSync(join(DIST_FOLDER, 'browser', 'index.html')).toString();
const win = domino.createWindow(template);

global['window'] = win;
global['document'] = win.document;
global['DOMTokenList'] = win.DOMTokenList;
global['Node'] = win.Node;
global['Text'] = win.Text;
global['HTMLElement'] = win.HTMLElement;
global['navigator'] = win.navigator;
global['CSS'] = null;
global['Event'] = win.Event;
global['Event']['prototype'] = win.Event.prototype;

Object.defineProperty(win.document.body.style, 'transform', {
  value: () => {
    return {
      enumerable: true,
      configurable: true
    };
  },
});

// Express server
const app = express();

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

const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main');
const { provideModuleMap } = require('@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'));

// 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('index', { req });
});

// Start up the Node server
app.listen(PORT, () => {
  console.log(`Node server listening on http://localhost:${PORT}`);
});

and webpack.server.config.js

const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: { server: './server.ts' },
  resolve: { extensions: ['.js', '.ts'] },
  target: 'node',
  mode: 'none',
  // this makes sure we includes 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' },
      {
        // Mark files inside `@angular/core` as using SystemJS style dynamic imports.
        // Removing this will cause deprecation warnings to appear.
        test: /(\\|\/)@angular(\\|\/)core(\\|\/).+\.js$/,
        parser: { system: true },
      },
    ]
  },
  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'),
      {}
    ),
    new webpack.ProvidePlugin({
      $: "jquery",
      jQuery: "jquery",
      "window.jQuery": "jquery"
    })
  ]
};

npm run build:ssr && npm run serve:ssr -> http://localhost:4000 -> keeps loading -> ERR_EMPTY_RESPONSE

2
Hi Saad Rashid, Can you solve this issue. If yes please provide a answer.Harleen Kaur Arora
Hi Harleen, Yea I solved it after a few more hours of debugging. The underlying problem was with how our angular 7 app was using browser types in some life-cycle hooks. I started from app.component.ts and tested each component below it and made my ngoninit client specific code to only run if the platform is browser.Saad Rashid
I am facing the same issue. Can you please explain it more?Pritam Kadam
Hi Pritam, you should check if there's any browser specific code or API method being used in your application which you are trying to run via Node?Saad Rashid
If you have used browrser types such as window, document, or localStorage etc. You'll need to wrap these in a conditional statement where you make sure they only run when you're angular app is running in a browser if (isPlatformBrowser(this.platformId)) { // Client only code }Saad Rashid

2 Answers

2
votes

I was able to fix this by making sure my angular application is platform agnostic i.e by making browser specific code, browser API methods or browser types such as window, document, or localStorage only run in the browser.

for eg: if (isPlatformBrowser(this.platformId)) { // Client only code }

1
votes

To add on to this you also need to wrap setTimeout and location with the check against the platform browser. I found this gist which should let you see how to set everything up.