Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular Universal express server keeps loading and fails with ERR_EMPTY_RESPONSE

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

like image 716
Saad Rashid Avatar asked Feb 14 '19 22:02

Saad Rashid


2 Answers

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 }

like image 155
Saad Rashid Avatar answered Nov 09 '22 10:11

Saad Rashid


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.

like image 1
Tri42 Avatar answered Nov 09 '22 11:11

Tri42