From 68d585a416b58e2c2209953f600c3fd3bb5b3427 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Fri, 22 Sep 2017 19:48:47 -0400 Subject: [PATCH 1/7] docs(universal): update docs --- .../stories/universal-rendering.md | 174 ++++++++++++++---- 1 file changed, 143 insertions(+), 31 deletions(-) diff --git a/docs/documentation/stories/universal-rendering.md b/docs/documentation/stories/universal-rendering.md index f28e1916af76..a5af7cad123b 100644 --- a/docs/documentation/stories/universal-rendering.md +++ b/docs/documentation/stories/universal-rendering.md @@ -1,19 +1,35 @@ -# Universal bundles +# Angular Universal Integration -Angular CLI supports generation of a Universal build for your application. This is a CommonJS-formatted bundle which can be `require()`'d into a Node application (for example, an Express server) and used with `@angular/platform-server`'s APIs to prerender your application. +The Angular CLI supports generation of a Universal build for your application. This is a CommonJS-formatted bundle which can be `require()`'d into a Node application (for example, an Express server) and used with `@angular/platform-server`'s APIs to prerender your application. + +--- + +### Example CLI Integration: + +[Angular Universal-Starter](https://github.com/angular/universal-starter/tree/master/cli) - Clone the universal-starter, and check out the `/cli` folder for a working example. + +--- + +# Integrating Angular Universal into existing CLI Applications This story will show you how to set up Universal bundling for an existing `@angular/cli` project in 4 steps. +**Want to see the Angular CLI with Universal in Action?** + +[Check out the Angular Universal-Starter (/cli folder)](https://github.com/angular/universal-starter) + +--- + ## Step 0: Install `@angular/platform-server` Install `@angular/platform-server` into your project. Make sure you use the same version as the other `@angular` packages in your project. ```bash -$ npm install --save-dev @angular/platform-server +$ npm install --save @angular/platform-server ``` or ```bash -$ yarn add @angular/platform-server --dev +$ yarn add @angular/platform-server ``` @@ -46,7 +62,7 @@ This example places it alongside `app.module.ts` in a file named `app.server.mod ### src/app/app.server.module.ts: -```javascript +```typescript import {NgModule} from '@angular/core'; import {ServerModule} from '@angular/platform-server'; @@ -67,13 +83,14 @@ import {AppComponent} from './app.component'; export class AppServerModule {} ``` -## Step 2: Create a server main file and tsconfig to build it +--- +## 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: -```javascript +```typescript export {AppServerModule} from './app/app.server.module'; ``` @@ -105,6 +122,7 @@ Add a section for `"angularCompilerOptions"` and set `"entryModule"` to your `Ap } ``` +--- ## 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 key `"platform"` set to `"server"`. @@ -118,16 +136,17 @@ Then, remove the `"polyfills"` key - those aren't needed on the server, and adju ... "apps": [ { - // Keep your original application config intact here. - // It will be app 0. + // 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. It is app 1. "platform": "server", "root": "src", - // Build to dist-server instead of dist. This prevents + // Build to dist/server instead of dist. This prevents // client and server builds from overwriting each other. - "outDir": "dist-server", + "outDir": "dist/server", "assets": [ "assets", "favicon.ico" @@ -163,11 +182,13 @@ Then, remove the `"polyfills"` key - those aren't needed on the server, and adju 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 its index of `1` in the `"apps"` array in `.angular-cli.json`: ```bash -# This builds the client application in dist/ +# This builds the client application in dist/browser/ $ ng build --prod ... -# This builds the server bundle in dist-server/ -$ ng build --prod --app 1 +# This builds the server bundle in dist/server/ +$ ng build --prod --app 1 --output + +# outputs: Date: 2017-07-24T22:42:09.739Z Hash: 9cac7d8e9434007fd8da Time: 4933ms @@ -175,29 +196,120 @@ chunk {0} main.988d7a161bd984b7eb54.bundle.js (main) 9.49 kB [entry] [rendered] chunk {1} styles.d41d8cd98f00b204e980.bundle.css (styles) 0 bytes [entry] [rendered] ``` -## Testing the bundle +--- -With this bundle built, you can use `renderModuleFactory` from `@angular/platform-server` to test it out. -```javascript -// Load zone.js for the server. -require('zone.js/dist/zone-node'); +## Step 4: Setting up an Express Server to run our Universal bundles + +Now that we have everything set up to -make- the bundles, how we get everything running? + +PlatformServer offers a method called `renderModuleFactory()` that we can use to pass in our AoT'd AppServerModule, to serialize our application, and then we'll be returning that result to the Browser. + +```typescript +app.engine('html', (_, options, callback) => { + renderModuleFactory(AppServerModuleNgFactory, { + // Our index.html + document: template, + url: options.req.url, + // DI so that we can get lazy-loading to work differently (since we need it to just instantly render it) + extraProviders: [ + provideModuleMap(LAZY_MODULE_MAP) + ] + }).then(html => { + callback(null, html); + }); +}); +``` + +You could do this, if you want complete flexibility, or use an express-engine with a few other built in features from [`@nguniversal/express-engine`](https://github.com/angular/universal/tree/master/modules/express-engine) found here. + +```typescript +// It's used as easily as +import { ngExpressEngine } from '@nguniversal/express-engine'; -// Import renderModuleFactory from @angular/platform-server. -var renderModuleFactory = require('@angular/platform-server').renderModuleFactory; +app.engine('html', ngExpressEngine({ + bootstrap: AppServerModuleNgFactory, + providers: [ + provideModuleMap(LAZY_MODULE_MAP) + ] +})); +``` + +Below we can see a TypeScript implementation of a -very- simple Express server to fire everything up. + +> Note: This is a very bare bones Express 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! + + +```typescript +// These are important and needed before anything else +import 'zone.js/dist/zone-node'; +import 'reflect-metadata'; + +import { renderModuleFactory } from '@angular/platform-server'; +import { enableProdMode } from '@angular/core'; + +import * as express from 'express'; +import { join } from 'path'; +import { readFileSync } from 'fs'; + +// Faster server renders w/ Prod mode (dev mode never needed) +enableProdMode(); + +// Express server +const app = express(); -// Import the AOT compiled factory for your AppServerModule. -// This import will change with the hash of your built server bundle. -var AppServerModuleNgFactory = require('./dist-server/main.988d7a161bd984b7eb54.bundle').AppServerModuleNgFactory; +const PORT = process.env.PORT || 4000; +const DIST_FOLDER = join(process.cwd(), 'dist'); -// Load the index.html file. -var index = require('fs').readFileSync('./src/index.html', 'utf8'); +// Our index.html we'll use as our template +const template = readFileSync(join(DIST_FOLDER, 'browser', 'index.html')).toString(); -// Render to HTML and log it to the console. -renderModuleFactory(AppServerModuleNgFactory, {document: index, url: '/'}).then(html => console.log(html)); +// * NOTE :: leave this as require() since this file is built Dynamically from webpack +const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main.bundle'); + +const { provideModuleMap } = require('@nguniversal/module-map-ngfactory-loader'); + +app.engine('html', (_, options, callback) => { + renderModuleFactory(AppServerModuleNgFactory, { + // Our index.html + document: template, + url: options.req.url, + // DI so that we can get lazy-loading to work differently (since we need it to just instantly render it) + extraProviders: [ + provideModuleMap(LAZY_MODULE_MAP) + ] + }).then(html => { + callback(null, html); + }); +}); + +app.set('view engine', 'html'); +app.set('views', 'src'); + +// app.get('/sitemap.txt', express.static(DIST_FOLDER)); + +// 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}`); +}); ``` -## Caveats +## Gotchas & Caveats + +[Full list of important Gotchas available here](https://github.com/angular/universal#universal-gotchas) + +Brief summary of a few important gotchas: -* Lazy loading is not yet supported, but coming very soon. Currently lazy loaded routes aren't available for prerendering, and you will get a `System is not defined` error. -* The bundle produced has a hash in the filename from webpack. When deploying this to a production server, you will need to ensure the correct bundle is required, either by renaming the file or passing the bundle name as an argument to your server. \ No newline at end of file +- Http calls will be executed **twice** + - Once on the server, and again on the client +- window/document/location/localStorage/etc **do not exist on the server** +- Avoid or limit setTimeout/setInterval use +- Never manipulate the DOM directly (it doesn't exist on the server) \ No newline at end of file From 0cea1fea742ad464b4367538090c464ba75fe883 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Fri, 22 Sep 2017 19:58:05 -0400 Subject: [PATCH 2/7] docs(universal): updates --- .../stories/universal-rendering.md | 85 +++++++++++++++++-- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/docs/documentation/stories/universal-rendering.md b/docs/documentation/stories/universal-rendering.md index a5af7cad123b..7f30c4925896 100644 --- a/docs/documentation/stories/universal-rendering.md +++ b/docs/documentation/stories/universal-rendering.md @@ -4,7 +4,7 @@ The Angular CLI supports generation of a Universal build for your application. T --- -### Example CLI Integration: +## Example CLI Integration: [Angular Universal-Starter](https://github.com/angular/universal-starter/tree/master/cli) - Clone the universal-starter, and check out the `/cli` folder for a working example. @@ -12,7 +12,7 @@ The Angular CLI supports generation of a Universal build for your application. T # Integrating Angular Universal into existing CLI Applications -This story will show you how to set up Universal bundling for an existing `@angular/cli` project in 4 steps. +This story will show you how to set up Universal bundling for an existing `@angular/cli` project in 5 steps. **Want to see the Angular CLI with Universal in Action?** @@ -239,6 +239,7 @@ Below we can see a TypeScript implementation of a -very- simple Express server t > Note: This is a very bare bones Express 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`** ```typescript // These are important and needed before anything else @@ -286,8 +287,6 @@ app.engine('html', (_, options, callback) => { app.set('view engine', 'html'); app.set('views', 'src'); -// app.get('/sitemap.txt', express.static(DIST_FOLDER)); - // Server static files from /browser app.get('*.*', express.static(join(DIST_FOLDER, 'browser'))); @@ -302,7 +301,83 @@ app.listen(PORT, () => { }); ``` -## Gotchas & Caveats +## Step 5: Setup a webpack config to handle this Node server.ts file and serve your application! + +Now that we have our Node Express server 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.ts file, and takes it and compiles it and every dependency it has into `dist/server.js`. + +```typescript +const path = require('path'); + +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' } + ] + } +} +``` + +**Almost 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.js +``` + +To fire up the application, in your terminal enter + +```bash +node dist/server.js +``` + +**Tada!** + +Now we can create a few handy scripts to help us do all of this in the future. + +```json +"scripts": { + // your other scripts + "build:client-and-server-bundles": "ng build --prod && ng build --prod --app 1 --output-hashing=false", + "build:dynamic": "npm run build:client-and-server-bundles && npm run webpack:server", + "webpack:server": "webpack --config webpack.server.config.js --progress --colors", + "serve:dynamic": "node dist/server.js" +} +``` + +In the future when you want to see a Production build of your app (locally), you can simply run + +```bash +npm run build:dynamic && npm run serve:dynamic +``` + +Enjoy! + +Once again to see a working version of everything, check out the [universal-starter](https://github.com/angular/universal-starter/). + +--- + +# Gotchas & Caveats [Full list of important Gotchas available here](https://github.com/angular/universal#universal-gotchas) From 70d814a2f439d26d6d6ffbae55191e49353b6b85 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Fri, 22 Sep 2017 20:04:06 -0400 Subject: [PATCH 3/7] remove duplicate universal-starter link --- docs/documentation/stories/universal-rendering.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/documentation/stories/universal-rendering.md b/docs/documentation/stories/universal-rendering.md index 7f30c4925896..501f80081983 100644 --- a/docs/documentation/stories/universal-rendering.md +++ b/docs/documentation/stories/universal-rendering.md @@ -14,10 +14,6 @@ The Angular CLI supports generation of a Universal build for your application. T This story will show you how to set up Universal bundling for an existing `@angular/cli` project in 5 steps. -**Want to see the Angular CLI with Universal in Action?** - -[Check out the Angular Universal-Starter (/cli folder)](https://github.com/angular/universal-starter) - --- ## Step 0: Install `@angular/platform-server` From de27c0e69e174512cda7fa0b282a9ff75c51f85d Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Sat, 23 Sep 2017 08:50:48 -0400 Subject: [PATCH 4/7] docs: update with changes requested --- .../stories/universal-rendering.md | 66 +++++++++++-------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/docs/documentation/stories/universal-rendering.md b/docs/documentation/stories/universal-rendering.md index 501f80081983..a4b67d529151 100644 --- a/docs/documentation/stories/universal-rendering.md +++ b/docs/documentation/stories/universal-rendering.md @@ -16,20 +16,18 @@ This story will show you how to set up Universal bundling for an existing `@angu --- -## Step 0: Install `@angular/platform-server` +## 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'll also need @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) + ```bash -$ npm install --save @angular/platform-server -``` -or -```bash -$ yarn add @angular/platform-server +$ npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ``` -## Step 1: Prepare your app for Universal rendering +## Step 1: Prepare your App for Universal rendering The first thing you need to do is make your `AppModule` compatible with Universal by addding `.withServerTransition()` and an application ID to your `BrowserModule` import: @@ -87,7 +85,7 @@ Create a main file for your Universal bundle. This file only needs to export you ### src/main.server.ts: ```typescript -export {AppServerModule} from './app/app.server.module'; +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"`. @@ -235,7 +233,9 @@ Below we can see a TypeScript implementation of a -very- simple Express server t > Note: This is a very bare bones Express 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`** +At the ROOT level of your project (where package.json / etc are), created a file named: **`server.ts`** + +### ./server.ts (root project level) ```typescript // These are important and needed before anything else @@ -286,7 +286,7 @@ app.set('views', 'src'); // Server static files from /browser app.get('*.*', express.static(join(DIST_FOLDER, 'browser'))); -// ALl regular routes use the Universal engine +// All regular routes use the Universal engine app.get('*', (req, res) => { res.render('index', { req }); }); @@ -305,16 +305,15 @@ Create a file named `webpack.server.config.js` at the ROOT of your application. > This file basically takes that server.ts file, and takes it and compiles it and every dependency it has into `dist/server.js`. +### ./webpack.server.config.js (root project level) + ```typescript const path = require('path'); +const webpack = require('webpack'); module.exports = { - entry: { - server: './server.ts' - }, - resolve: { - extensions: ['.ts', '.js'] - }, + 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)/], @@ -326,7 +325,20 @@ module.exports = { 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( + /(.+)?express(\\|\/)(.+)?/, + path.join(__dirname, 'src'), + ) + ] } ``` @@ -338,7 +350,6 @@ Now let's see what our resulting structure should look like, if we open up our ` /dist/ /browser/ /server/ - server.js ``` To fire up the application, in your terminal enter @@ -347,21 +358,24 @@ To fire up the application, in your terminal enter node dist/server.js ``` -**Tada!** +:sparkles: -Now we can create a few handy scripts to help us do all of this in the future. +Now lets create a few handy scripts to help us do all of this in the future. ```json "scripts": { - // your other scripts - "build:client-and-server-bundles": "ng build --prod && ng build --prod --app 1 --output-hashing=false", + + // These will be your common scripts "build:dynamic": "npm run build:client-and-server-bundles && npm run webpack:server", - "webpack:server": "webpack --config webpack.server.config.js --progress --colors", - "serve:dynamic": "node dist/server.js" + "serve:dynamic": "node dist/server.js", + + // Helpers for the above scripts + "build:client-and-server-bundles": "ng build --prod && ng build --prod --app 1 --output-hashing=false", + "webpack:server": "webpack --config webpack.server.config.js --progress --colors" } ``` -In the future when you want to see a Production build of your app (locally), you can simply run +In the future when you want to see a Production build of your app with Universal (locally), you can simply run: ```bash npm run build:dynamic && npm run serve:dynamic @@ -369,7 +383,7 @@ npm run build:dynamic && npm run serve:dynamic Enjoy! -Once again to see a working version of everything, check out the [universal-starter](https://github.com/angular/universal-starter/). +Once again to see a working version of everything, check out the [universal-starter](https://github.com/angular/universal-starter/tree/master/cli). --- From 667630a9075862cf9701a52468d3843d074fbfc8 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Fri, 22 Sep 2017 19:48:47 -0400 Subject: [PATCH 5/7] docs(universal): update docs docs(universal): updates remove duplicate universal-starter link pick pocs: update with changes requested --- .../stories/universal-rendering.md | 273 +++++++++++++++--- 1 file changed, 235 insertions(+), 38 deletions(-) diff --git a/docs/documentation/stories/universal-rendering.md b/docs/documentation/stories/universal-rendering.md index f28e1916af76..a4b67d529151 100644 --- a/docs/documentation/stories/universal-rendering.md +++ b/docs/documentation/stories/universal-rendering.md @@ -1,23 +1,33 @@ -# Universal bundles +# Angular Universal Integration -Angular CLI supports generation of a Universal build for your application. This is a CommonJS-formatted bundle which can be `require()`'d into a Node application (for example, an Express server) and used with `@angular/platform-server`'s APIs to prerender your application. +The Angular CLI supports generation of a Universal build for your application. This is a CommonJS-formatted bundle which can be `require()`'d into a Node application (for example, an Express server) and used with `@angular/platform-server`'s APIs to prerender your application. -This story will show you how to set up Universal bundling for an existing `@angular/cli` project in 4 steps. +--- -## Step 0: Install `@angular/platform-server` +## Example CLI Integration: + +[Angular Universal-Starter](https://github.com/angular/universal-starter/tree/master/cli) - Clone the universal-starter, and check out the `/cli` folder for a working example. + +--- + +# Integrating Angular Universal into existing CLI Applications + +This story will show you how to set up Universal bundling for an existing `@angular/cli` project in 5 steps. + +--- + +## 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'll also need @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) + ```bash -$ npm install --save-dev @angular/platform-server -``` -or -```bash -$ yarn add @angular/platform-server --dev +$ npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ``` -## Step 1: Prepare your app for Universal rendering +## Step 1: Prepare your App for Universal rendering The first thing you need to do is make your `AppModule` compatible with Universal by addding `.withServerTransition()` and an application ID to your `BrowserModule` import: @@ -46,7 +56,7 @@ This example places it alongside `app.module.ts` in a file named `app.server.mod ### src/app/app.server.module.ts: -```javascript +```typescript import {NgModule} from '@angular/core'; import {ServerModule} from '@angular/platform-server'; @@ -67,14 +77,15 @@ import {AppComponent} from './app.component'; export class AppServerModule {} ``` -## Step 2: Create a server main file and tsconfig to build it +--- +## 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: -```javascript -export {AppServerModule} from './app/app.server.module'; +```typescript +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"`. @@ -105,6 +116,7 @@ Add a section for `"angularCompilerOptions"` and set `"entryModule"` to your `Ap } ``` +--- ## 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 key `"platform"` set to `"server"`. @@ -118,16 +130,17 @@ Then, remove the `"polyfills"` key - those aren't needed on the server, and adju ... "apps": [ { - // Keep your original application config intact here. - // It will be app 0. + // 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. It is app 1. "platform": "server", "root": "src", - // Build to dist-server instead of dist. This prevents + // Build to dist/server instead of dist. This prevents // client and server builds from overwriting each other. - "outDir": "dist-server", + "outDir": "dist/server", "assets": [ "assets", "favicon.ico" @@ -163,11 +176,13 @@ Then, remove the `"polyfills"` key - those aren't needed on the server, and adju 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 its index of `1` in the `"apps"` array in `.angular-cli.json`: ```bash -# This builds the client application in dist/ +# This builds the client application in dist/browser/ $ ng build --prod ... -# This builds the server bundle in dist-server/ -$ ng build --prod --app 1 +# This builds the server bundle in dist/server/ +$ ng build --prod --app 1 --output + +# outputs: Date: 2017-07-24T22:42:09.739Z Hash: 9cac7d8e9434007fd8da Time: 4933ms @@ -175,29 +190,211 @@ chunk {0} main.988d7a161bd984b7eb54.bundle.js (main) 9.49 kB [entry] [rendered] chunk {1} styles.d41d8cd98f00b204e980.bundle.css (styles) 0 bytes [entry] [rendered] ``` -## Testing the bundle +--- -With this bundle built, you can use `renderModuleFactory` from `@angular/platform-server` to test it out. -```javascript -// Load zone.js for the server. -require('zone.js/dist/zone-node'); +## Step 4: Setting up an Express Server to run our Universal bundles + +Now that we have everything set up to -make- the bundles, how we get everything running? + +PlatformServer offers a method called `renderModuleFactory()` that we can use to pass in our AoT'd AppServerModule, to serialize our application, and then we'll be returning that result to the Browser. + +```typescript +app.engine('html', (_, options, callback) => { + renderModuleFactory(AppServerModuleNgFactory, { + // Our index.html + document: template, + url: options.req.url, + // DI so that we can get lazy-loading to work differently (since we need it to just instantly render it) + extraProviders: [ + provideModuleMap(LAZY_MODULE_MAP) + ] + }).then(html => { + callback(null, html); + }); +}); +``` + +You could do this, if you want complete flexibility, or use an express-engine with a few other built in features from [`@nguniversal/express-engine`](https://github.com/angular/universal/tree/master/modules/express-engine) found here. -// Import renderModuleFactory from @angular/platform-server. -var renderModuleFactory = require('@angular/platform-server').renderModuleFactory; +```typescript +// It's used as easily as +import { ngExpressEngine } from '@nguniversal/express-engine'; -// Import the AOT compiled factory for your AppServerModule. -// This import will change with the hash of your built server bundle. -var AppServerModuleNgFactory = require('./dist-server/main.988d7a161bd984b7eb54.bundle').AppServerModuleNgFactory; +app.engine('html', ngExpressEngine({ + bootstrap: AppServerModuleNgFactory, + providers: [ + provideModuleMap(LAZY_MODULE_MAP) + ] +})); +``` + +Below we can see a TypeScript implementation of a -very- simple Express server to fire everything up. + +> Note: This is a very bare bones Express 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) + +```typescript +// These are important and needed before anything else +import 'zone.js/dist/zone-node'; +import 'reflect-metadata'; + +import { renderModuleFactory } from '@angular/platform-server'; +import { enableProdMode } from '@angular/core'; + +import * as express from 'express'; +import { join } from 'path'; +import { readFileSync } from 'fs'; + +// Faster server renders w/ Prod mode (dev mode never needed) +enableProdMode(); -// Load the index.html file. -var index = require('fs').readFileSync('./src/index.html', 'utf8'); +// Express server +const app = express(); -// Render to HTML and log it to the console. -renderModuleFactory(AppServerModuleNgFactory, {document: index, url: '/'}).then(html => console.log(html)); +const PORT = process.env.PORT || 4000; +const DIST_FOLDER = join(process.cwd(), 'dist'); + +// Our index.html we'll use as our template +const template = readFileSync(join(DIST_FOLDER, 'browser', 'index.html')).toString(); + +// * NOTE :: leave this as require() since this file is built Dynamically from webpack +const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main.bundle'); + +const { provideModuleMap } = require('@nguniversal/module-map-ngfactory-loader'); + +app.engine('html', (_, options, callback) => { + renderModuleFactory(AppServerModuleNgFactory, { + // Our index.html + document: template, + url: options.req.url, + // DI so that we can get lazy-loading to work differently (since we need it to just instantly render it) + extraProviders: [ + provideModuleMap(LAZY_MODULE_MAP) + ] + }).then(html => { + callback(null, html); + }); +}); + +app.set('view engine', 'html'); +app.set('views', 'src'); + +// 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}`); +}); ``` -## Caveats +## Step 5: Setup a webpack config to handle this Node server.ts file and serve your application! + +Now that we have our Node Express server 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.ts file, and takes it and compiles it and every dependency it has into `dist/server.js`. + +### ./webpack.server.config.js (root project level) + +```typescript +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( + /(.+)?express(\\|\/)(.+)?/, + path.join(__dirname, 'src'), + ) + ] +} +``` + +**Almost 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/ +``` + +To fire up the application, in your terminal enter + +```bash +node dist/server.js +``` + +:sparkles: + +Now lets create a few handy scripts to help us do all of this in the future. + +```json +"scripts": { + + // These will be your common scripts + "build:dynamic": "npm run build:client-and-server-bundles && npm 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 1 --output-hashing=false", + "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: + +```bash +npm run build:dynamic && npm run serve:dynamic +``` + +Enjoy! + +Once again to see a working version of everything, check out the [universal-starter](https://github.com/angular/universal-starter/tree/master/cli). + +--- + +# Gotchas & Caveats + +[Full list of important Gotchas available here](https://github.com/angular/universal#universal-gotchas) + +Brief summary of a few important gotchas: -* Lazy loading is not yet supported, but coming very soon. Currently lazy loaded routes aren't available for prerendering, and you will get a `System is not defined` error. -* The bundle produced has a hash in the filename from webpack. When deploying this to a production server, you will need to ensure the correct bundle is required, either by renaming the file or passing the bundle name as an argument to your server. \ No newline at end of file +- Http calls will be executed **twice** + - Once on the server, and again on the client +- window/document/location/localStorage/etc **do not exist on the server** +- Avoid or limit setTimeout/setInterval use +- Never manipulate the DOM directly (it doesn't exist on the server) \ No newline at end of file From e3e5ff0204364000d253c27486dc3476b7185ee3 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Sat, 23 Sep 2017 11:59:27 -0400 Subject: [PATCH 6/7] update views dist_folder --- docs/documentation/stories/universal-rendering.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/documentation/stories/universal-rendering.md b/docs/documentation/stories/universal-rendering.md index a4b67d529151..dc353777b5b7 100644 --- a/docs/documentation/stories/universal-rendering.md +++ b/docs/documentation/stories/universal-rendering.md @@ -281,7 +281,7 @@ app.engine('html', (_, options, callback) => { }); app.set('view engine', 'html'); -app.set('views', 'src'); +app.set('views', join(DIST_FOLDER, 'browser')); // Server static files from /browser app.get('*.*', express.static(join(DIST_FOLDER, 'browser'))); From b5a107e401d505652e5c653ed6e6f8c9da58fc5d Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Fri, 22 Sep 2017 19:48:47 -0400 Subject: [PATCH 7/7] docs(universal): update docs docs(universal): updates remove duplicate universal-starter link pick pocs: update with changes requested docs(universal): updates remove duplicate universal-starter link docs: update with changes requested update views dist_folder --- .../stories/universal-rendering.md | 128 +++++++++++++++--- 1 file changed, 108 insertions(+), 20 deletions(-) diff --git a/docs/documentation/stories/universal-rendering.md b/docs/documentation/stories/universal-rendering.md index a5af7cad123b..17a5e616d41d 100644 --- a/docs/documentation/stories/universal-rendering.md +++ b/docs/documentation/stories/universal-rendering.md @@ -4,7 +4,7 @@ The Angular CLI supports generation of a Universal build for your application. T --- -### Example CLI Integration: +## Example CLI Integration: [Angular Universal-Starter](https://github.com/angular/universal-starter/tree/master/cli) - Clone the universal-starter, and check out the `/cli` folder for a working example. @@ -12,28 +12,25 @@ The Angular CLI supports generation of a Universal build for your application. T # Integrating Angular Universal into existing CLI Applications -This story will show you how to set up Universal bundling for an existing `@angular/cli` project in 4 steps. - -**Want to see the Angular CLI with Universal in Action?** - -[Check out the Angular Universal-Starter (/cli folder)](https://github.com/angular/universal-starter) +This story will show you how to set up Universal bundling for an existing `@angular/cli` project in 5 steps. --- -## Step 0: Install `@angular/platform-server` +## 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'll also need @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) + ```bash -$ npm install --save @angular/platform-server -``` -or -```bash -$ yarn add @angular/platform-server +$ npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ``` +<<<<<<< HEAD +======= -## Step 1: Prepare your app for Universal rendering +>>>>>>> de27c0e6... docs: update with changes requested +## Step 1: Prepare your App for Universal rendering The first thing you need to do is make your `AppModule` compatible with Universal by addding `.withServerTransition()` and an application ID to your `BrowserModule` import: @@ -91,7 +88,7 @@ Create a main file for your Universal bundle. This file only needs to export you ### src/main.server.ts: ```typescript -export {AppServerModule} from './app/app.server.module'; +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"`. @@ -239,6 +236,9 @@ Below we can see a TypeScript implementation of a -very- simple Express server t > Note: This is a very bare bones Express 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) ```typescript // These are important and needed before anything else @@ -284,16 +284,14 @@ app.engine('html', (_, options, callback) => { }); app.set('view engine', 'html'); -app.set('views', 'src'); - -// app.get('/sitemap.txt', express.static(DIST_FOLDER)); +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 +// All regular routes use the Universal engine app.get('*', (req, res) => { - res.render('index', { req }); + res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req }); }); // Start up the Node server @@ -302,7 +300,97 @@ app.listen(PORT, () => { }); ``` -## Gotchas & Caveats +## Step 5: Setup a webpack config to handle this Node server.ts file and serve your application! + +Now that we have our Node Express server 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.ts file, and takes it and compiles it and every dependency it has into `dist/server.js`. + +### ./webpack.server.config.js (root project level) + +```typescript +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( + /(.+)?express(\\|\/)(.+)?/, + path.join(__dirname, 'src'), + ) + ] +} +``` + +**Almost 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/ +``` + +To fire up the application, in your terminal enter + +```bash +node dist/server.js +``` + +:sparkles: + +Now lets create a few handy scripts to help us do all of this in the future. + +```json +"scripts": { + + // These will be your common scripts + "build:dynamic": "npm run build:client-and-server-bundles && npm 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 1 --output-hashing=false", + "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: + +```bash +npm run build:dynamic && npm run serve:dynamic +``` + +Enjoy! + +Once again to see a working version of everything, check out the [universal-starter](https://github.com/angular/universal-starter/tree/master/cli). + +--- + +# Gotchas & Caveats [Full list of important Gotchas available here](https://github.com/angular/universal#universal-gotchas)