Package Exports
- @hapiness/ng-universal
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@hapiness/ng-universal) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
NG-Universal
This is a Hapiness Engine for running Angular Apps on the server for server side rendering.
Integrating NG-Universal into existing CLI Applications
This story will show you how to set up Universal bundling for an existing @angular/cli.
We support actually @angular @5.2.3 and next so you must upgrade all packages inside your project.
We use yarn as package manager.
Table of contents
- Install Dependencies
- Step 1: Prepare your App for Universal rendering
- Step 2: Create a server "main" file and tsconfig to build it
- Step 3: Create a new project in .angular-cli.json
- Step 4: Setting up a Hapiness Application to run our Universal bundles
- Step 5: Setup a webpack config to handle this Node server.ts file and serve your application!
- Contributing
- Change History
- Maintainers
- License
Install Dependencies
Install @angular/platform-server into your project. Make sure you use the same version as the other @angular packages in your project.
You also need :
ts-loaderfor your webpack build we'll show later and it's only indevDependencies.@nguniversal/module-map-ngfactory-loader, as it's used to handle lazy-loading in the context of a server-render. (by loading the chunks right away)
Install Hapiness modules into your project: @hapiness/core, @hapiness/ng-universal and @hapiness/ng-universal-transfer-http.
$ yarn add --dev ts-loader
$ yarn add @angular/platform-server @nguniversal/module-map-ngfactory-loader @hapiness/core @hapiness/ng-universal @hapiness/ng-universal-transfer-httpStep 1: Prepare your App for Universal rendering
The first thing you need to do is make your AppModule compatible with Universal by adding .withServerTransition() and an application ID to your BrowserModule import.
TransferHttpCacheModule installs a Http interceptor that avoids duplicate HttpClient requests on the client, for requests that were already made when the application was rendered on the server side.
When the module is installed in the application NgModule, it will intercept HttpClient requests on the server and store the response in the TransferState key-value store. This is transferred to the client, which then uses it to respond to the same HttpClient requests on the client.
To use the TransferHttpCacheModule just install it as part of the top-level App module.
src/app/app.module.ts:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { TransferHttpCacheModule } from '@hapiness/ng-universal-transfer-http';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
// Add .withServerTransition() to support Universal rendering.
// The application ID can be any identifier which is unique on
// the page.
BrowserModule.withServerTransition({ appId: 'ng-universal-example' }),
// Add TransferHttpCacheModule to install a Http interceptor
TransferHttpCacheModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule {
}Next, create a module specifically for your application when running on the server. It's recommended to call this module AppServerModule.
This example places it alongside app.module.ts in a file named app.server.module.ts:
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: [
// The AppServerModule should import your AppModule followed
// by the ServerModule from @angular/platform-server.
AppModule,
ServerModule,
ModuleMapLoaderModule,
ServerTransferStateModule
],
// Since the bootstrapped component is not inherited from your
// imported AppModule, it needs to be repeated here.
bootstrap: [AppComponent]
})
export class AppServerModule {
}Then, you must set an event on DOMContentLoaded to be sure TransferState will be passed between server and client.
src/main.ts:
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
document.addEventListener('DOMContentLoaded', () => {
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));
});Step 2: Create a server "main" file and tsconfig to build it
Create a main file for your Universal bundle. This file only needs to export your AppServerModule. It can go in src. This example calls this file main.server.ts:
src/main.server.ts:
export { AppServerModule } from './app/app.server.module';Copy tsconfig.app.json to tsconfig.server.json and change it to build with a "module" target of "commonjs".
Add a section for "angularCompilerOptions" and set "entryModule" to your AppServerModule, specified as a path to the import with a hash (#) containing the symbol name. In this example, this would be app/app.server.module#AppServerModule.
src/tsconfig.server.json:
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"baseUrl": "./",
// Set the module format to "commonjs":
"module": "commonjs",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
],
// Add "angularCompilerOptions" with the AppServerModule you wrote
// set as the "entryModule".
"angularCompilerOptions": {
"entryModule": "app/app.server.module#AppServerModule"
}
}
Step 3: Create a new project in .angular-cli.json
In .angular-cli.json there is an array under the key "apps". Copy the configuration for your client application there, and paste it as a new entry in the array, with an additional keys "platform" and "name" set to "server".
Then, remove the "polyfills" key - those aren't needed on the server, and adjust "main", and "tsconfig" to point to the files you wrote in step 2.
Finally, adjust "outDir" to a new location (this example uses dist/server).
.angular-cli.json:
{
...
"apps": [
{
// Keep your original application config intact here, this is app 0
// -EXCEPT- for outDir, udpate it to dist/browser
"outDir": "dist/browser" // <-- update this
},
{
// This is your server app.
"platform": "server",
"name": "server",
"root": "src",
// Build to dist/server instead of dist. This prevents
// client and server builds from overwriting each other.
"outDir": "dist/server",
"assets": [
"assets",
"favicon.ico"
],
"index": "index.html",
// Change the main file to point to your server main.
"main": "main.server.ts",
// Remove polyfills.
// "polyfills": "polyfills.ts",
"test": "test.ts",
// Change the tsconfig to point to your server config.
"tsconfig": "tsconfig.server.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"styles.css"
],
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
],
...
}Building the bundle:
With these steps complete, you should be able to build a server bundle for your application, using the --app flag to tell the CLI to build the server bundle, referencing with name "server" in the "apps" array in .angular-cli.json:
# This builds the client application in dist/browser/
$ ng build --prod
...
# This builds the server bundle in dist/server/
$ ng build --prod --app server --output-hashing=none
# outputs:
Date: 2017-10-21T21:54:49.240Z
Hash: 3034f2772435757f234a
Time: 3689ms
chunk {0} main.bundle.js (main) 9.2 kB [entry] [rendered]
chunk {1} styles.bundle.css (styles) 0 bytes [entry] [rendered]Step 4: Setting up a Hapiness Application to run our Universal bundles
Now that we have everything set up to -make- the bundles, how we get everything running?
We'll use Hapiness application and @hapiness/ng-universal module.
Below we can see a TypeScript implementation of a -very- simple Hapiness application to fire everything up.
Note:
This is a very bare bones Hapiness application, and is just for demonstrations sake.
In a real production environment, you'd want to make sure you have other authentication and security things setup here as well.
This is just meant just to show the specific things needed that are relevant to Universal itself. The rest is up to you!
At the ROOT level of your project (where package.json / etc are), created a file named: server.ts
server.ts (root project level):
// These are important and needed before anything else
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { enableProdMode } from '@angular/core';
import { Hapiness, HapinessModule, HttpServerExt, HttpServerService, OnError, OnStart } from '@hapiness/core';
import { NgUniversalModule } from '@hapiness/ng-universal';
import { join } from 'path';
const BROWSER_FOLDER = join(process.cwd(), 'dist', 'browser');
// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main.bundle');
// Create our Hapiness application
@HapinessModule({
version: '1.0.0',
imports: [
NgUniversalModule.setConfig({
bootstrap: AppServerModuleNgFactory,
lazyModuleMap: LAZY_MODULE_MAP,
staticContent: {
indexFile: 'index.html',
rootPath: BROWSER_FOLDER
}
})
],
providers: [
HttpServerService
]
})
class HapinessApplication implements OnStart, OnError {
/**
* Class constructor
*
* @param {HttpServerService} _httpServer DI for HttpServerService to provide .instance() method to give original Hapi.js server
*/
constructor(private _httpServer: HttpServerService) {
}
/**
* OnStart process
*/
onStart(): void {
console.log(`Node server listening on ${this._httpServer.instance().info.uri}`);
}
/**
* OnError process
*/
onError(error: Error): void {
console.error(error);
}
}
// Boostrap Hapiness application
Hapiness.bootstrap(HapinessApplication, [
HttpServerExt.setConfig({
host: '0.0.0.0',
port: 4000
})
]);Extra Providers:
Extra Providers can be provided either on engine setup
NgUniversalModule.setConfig({
bootstrap: AppServerModuleNgFactory,
lazyModuleMap: LAZY_MODULE_MAP,
staticContent: {
indexFile: 'index.html',
rootPath: BROWSER_FOLDER
},
providers: [
ServerService
]
})Using the Request and Response:
The Request and Response objects are injected into the app via injection tokens. You can access them by @Inject
import { Request, REQUEST } from '@hapiness/ng-universal';
@Injectable()
export class RequestService {
constructor(@Inject(REQUEST) private _request: Request) {}
}If your app runs on the client side too, you will have to provide your own versions of these in the client app.
Step 5: Setup a webpack config to handle this Node server.ts file and serve your application!
Now that we have our Hapiness application setup, we need to pack it and serve it!
Create a file named webpack.server.config.js at the ROOT of your application.
This file basically takes that
server.tsfile, and takes it and compiles it and every dependency it has intodist/server.js.
./webpack.server.config.js (root project level):
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: { server: './server.ts' },
resolve: { extensions: ['.ts', '.js'] },
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(
/(.+)?hapiness(\\|\/)core(.+)?/,
path.join(__dirname, 'src'),
),
new webpack.ContextReplacementPlugin(
/(.+)?hapiness(\\|\/)ng-universal(.+)?/,
path.join(__dirname, 'src'),
)
],
stats: {
warnings: false
}
};Now, you can build your server file:
$ webpack --config webpack.server.config.js --progress --colorsAlmost there:
Now let's see what our resulting structure should look like, if we open up our /dist/ folder we should see:
/dist/
/browser/
/server/
server.jsTo fire up the application, in your terminal enter
$ node dist/server.jsNow lets create a few handy scripts to help us do all of this in the future.
"scripts": {
// These will be your common scripts
"build:dynamic": "yarn run build:client-and-server-bundles && yarn run webpack:server",
"serve:dynamic": "node dist/server.js",
// Helpers for the above scripts
"build:client-and-server-bundles": "ng build --prod && ng build --prod --app server --output-hashing=none",
"webpack:server": "webpack --config webpack.server.config.js --progress --colors"
}In the future when you want to see a Production build of your app with Universal (locally), you can simply run:
$ yarn run build:dynamic && yarn run serve:dynamicEnjoy!
Once again to see a working version of everything, check out the universal-starter.
Contributing
To set up your development environment:
- clone the repo to your workspace,
- in the shell
cdto the main folder, - hit
npm or yarn install, - run
npm or yarn run test.- It will lint the code and execute all tests.
- The test coverage report can be viewed from
./coverage/lcov-report/index.html.
Change History
- v5.2.3 (2018-02-01)
Angular v5.2.3+- Documentation
- v5.2.2 (2018-01-29)
Angular v5.2.2+- Documentation
- v5.2.1 (2017-12-20)
Angular v5.1.1+- Documentation
- v5.2.0 (2017-12-12)
Angular v5.1.0+RxJS v5.5.5+- Change
RESPONSE InjectionTokensignature to be areplyinterface fromHapiJS - Documentation
- v5.1.2 (2017-12-05)
Angular v5.0.5+RxJS v5.5.3+- Fix compilation of
Angular InjectionTokeninstances forRequestandResponseobjects
- v5.1.1 (2017-12-01)
Angular v5.0.4+- Change signature of
universal service responserelated to latest@hapiness/core
- v5.1.0 (2017-11-18)
Angular v5.0.2+- Returns
Observablein route instead of to useReplyNoContinueinterface - Update tests
- Change packaging process
- Documentation
- v5.0.0 (2017-11-13)
Angular v5.0.0+- Publish all features of the module
- Lettable operators for
RxJS - Tests
- Documentation
Maintainers
| Julien Fauville | Antoine Gomez | Sébastien Ritz | Nicolas Jessel | Mathieu Jeanmougin |
License
Copyright (c) 2018 Hapiness Licensed under the MIT license.