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
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 }
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With