|
| 1 | +# Universal bundles |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +This story will show you how to set up Universal bundling for an existing `@angular/cli` project in 4 steps. |
| 6 | + |
| 7 | +## Step 0: Install `@angular/platform-server` |
| 8 | + |
| 9 | +Install `@angular/platform-server` into your project. Make sure you use the same version as the other `@angular` packages in your project. |
| 10 | + |
| 11 | +```bash |
| 12 | +$ npm install --save-dev @angular/platform-server |
| 13 | +``` |
| 14 | +or |
| 15 | +```bash |
| 16 | +$ yarn add @angular/platform-server |
| 17 | +``` |
| 18 | + |
| 19 | + |
| 20 | +## Step 1: Prepare your app for Universal rendering |
| 21 | + |
| 22 | +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: |
| 23 | + |
| 24 | + |
| 25 | +### src/app/app.module.ts: |
| 26 | + |
| 27 | +```javascript |
| 28 | +@NgModule({ |
| 29 | + bootstrap: [AppComponent], |
| 30 | + imports: [ |
| 31 | + // Add .withServerTransition() to support Universal rendering. |
| 32 | + // The application ID can be any identifier which is unique on |
| 33 | + // the page. |
| 34 | + BrowserModule.withServerTransition({appId: 'my-app'}), |
| 35 | + ... |
| 36 | + ], |
| 37 | + |
| 38 | +}) |
| 39 | +export class AppModule {} |
| 40 | +``` |
| 41 | + |
| 42 | +Next, create a module specifically for your application when running on the server. It's recommended to call this module `AppServerModule`. |
| 43 | + |
| 44 | +This example places it alongside `app.module.ts` in a file named `app.server.module.ts`: |
| 45 | + |
| 46 | + |
| 47 | +### src/app/app.server.module.ts: |
| 48 | + |
| 49 | +```javascript |
| 50 | +import {NgModule} from '@angular/core'; |
| 51 | +import {ServerModule} from '@angular/platform-server'; |
| 52 | + |
| 53 | +import {AppModule} from './app.module'; |
| 54 | +import {AppComponent} from './app.component'; |
| 55 | + |
| 56 | +@NgModule({ |
| 57 | + imports: [ |
| 58 | + // The AppServerModule should import your AppModule followed |
| 59 | + // by the ServerModule from @angular/platform-server. |
| 60 | + AppModule, |
| 61 | + ServerModule, |
| 62 | + ], |
| 63 | + // Since the bootstrapped component is not inherited from your |
| 64 | + // imported AppModule, it needs to be repeated here. |
| 65 | + bootstrap: [AppComponent], |
| 66 | +}) |
| 67 | +export class AppServerModule {} |
| 68 | +``` |
| 69 | + |
| 70 | +## Step 2: Create a server main file and tsconfig to build it |
| 71 | + |
| 72 | +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`: |
| 73 | + |
| 74 | +### src/main.server.ts: |
| 75 | + |
| 76 | +```javascript |
| 77 | +export {AppServerModule} from './app/app.server.module'; |
| 78 | +``` |
| 79 | + |
| 80 | +Copy `tsconfig.app.json` to `tsconfig-server.json` and change it to build with a `"module"` target of `"commonjs"`. |
| 81 | + |
| 82 | +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`. |
| 83 | + |
| 84 | +### src/tsconfig.server.json: |
| 85 | + |
| 86 | +```javascript |
| 87 | +{ |
| 88 | + "extends": "../tsconfig.json", |
| 89 | + "compilerOptions": { |
| 90 | + "outDir": "../out-tsc/app", |
| 91 | + "baseUrl": "./", |
| 92 | + // Set the module format to "commonjs": |
| 93 | + "module": "commonjs", |
| 94 | + "types": [] |
| 95 | + }, |
| 96 | + "exclude": [ |
| 97 | + "test.ts", |
| 98 | + "**/*.spec.ts" |
| 99 | + ], |
| 100 | + // Add "angularCompilerOptions" with the AppServerModule you wrote |
| 101 | + // set as the "entryModule". |
| 102 | + "angularCompilerOptions": { |
| 103 | + "entryModule": "app/app.server.module#AppServerModule" |
| 104 | + } |
| 105 | +} |
| 106 | +``` |
| 107 | + |
| 108 | +## Step 3: Create a new project in `.angular-cli.json` |
| 109 | + |
| 110 | +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"`. |
| 111 | + |
| 112 | +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`). |
| 113 | + |
| 114 | +### .angular-cli.json: |
| 115 | + |
| 116 | +```javascript |
| 117 | +{ |
| 118 | + ... |
| 119 | + "apps": [ |
| 120 | + { |
| 121 | + // Keep your original application config intact here. |
| 122 | + // It will be app 0. |
| 123 | + }, |
| 124 | + { |
| 125 | + // This is your server app. It is app 1. |
| 126 | + "platform": "server", |
| 127 | + "root": "src", |
| 128 | + // Build to dist-server instead of dist. This prevents |
| 129 | + // client and server builds from overwriting each other. |
| 130 | + "outDir": "dist-server", |
| 131 | + "assets": [ |
| 132 | + "assets", |
| 133 | + "favicon.ico" |
| 134 | + ], |
| 135 | + "index": "index.html", |
| 136 | + // Change the main file to point to your server main. |
| 137 | + "main": "main.server.ts", |
| 138 | + // Remove polyfills. |
| 139 | + // "polyfills": "polyfills.ts", |
| 140 | + "test": "test.ts", |
| 141 | + // Change the tsconfig to point to your server config. |
| 142 | + "tsconfig": "tsconfig.server.json", |
| 143 | + "testTsconfig": "tsconfig.spec.json", |
| 144 | + "prefix": "app", |
| 145 | + "styles": [ |
| 146 | + "styles.css" |
| 147 | + ], |
| 148 | + "scripts": [], |
| 149 | + "environmentSource": "environments/environment.ts", |
| 150 | + "environments": { |
| 151 | + "dev": "environments/environment.ts", |
| 152 | + "prod": "environments/environment.prod.ts" |
| 153 | + } |
| 154 | + } |
| 155 | + ], |
| 156 | + ... |
| 157 | +} |
| 158 | + |
| 159 | +``` |
| 160 | + |
| 161 | +## Building the bundle |
| 162 | + |
| 163 | +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`: |
| 164 | + |
| 165 | +```bash |
| 166 | +# This builds the client application in dist/ |
| 167 | +$ ng build --prod |
| 168 | +... |
| 169 | +# This builds the server bundle in dist-server/ |
| 170 | +$ ng build --prod --app 1 |
| 171 | +Date: 2017-07-24T22:42:09.739Z |
| 172 | +Hash: 9cac7d8e9434007fd8da |
| 173 | +Time: 4933ms |
| 174 | +chunk {0} main.988d7a161bd984b7eb54.bundle.js (main) 9.49 kB [entry] [rendered] |
| 175 | +chunk {1} styles.d41d8cd98f00b204e980.bundle.css (styles) 0 bytes [entry] [rendered] |
| 176 | +``` |
| 177 | + |
| 178 | +## Testing the bundle |
| 179 | + |
| 180 | +With this bundle built, you can use `renderModuleFactory` from `@angular/platform-server` to test it out. |
| 181 | + |
| 182 | +```javascript |
| 183 | +// Load zone.js for the server. |
| 184 | +require('zone.js/dist/zone-node'); |
| 185 | + |
| 186 | +// Import renderModuleFactory from @angular/platform-server. |
| 187 | +var renderModuleFactory = require('@angular/platform-server').renderModuleFactory; |
| 188 | + |
| 189 | +// Import the AOT compiled factory for your AppServerModule. |
| 190 | +// This import will change with the hash of your built server bundle. |
| 191 | +var AppServerModuleNgFactory = require('./dist-server/main.988d7a161bd984b7eb54.bundle').AppServerModuleNgFactory; |
| 192 | + |
| 193 | +// Load the index.html file. |
| 194 | +var index = require('fs').readFileSync('./src/index.html', 'utf8'); |
| 195 | + |
| 196 | +// Render to HTML and log it to the console. |
| 197 | +renderModuleFactory(AppServerModuleNgFactory, {document: index, url: '/'}).then(html => console.log(html)); |
| 198 | +``` |
| 199 | + |
| 200 | +## Caveats |
| 201 | + |
| 202 | +* 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. |
| 203 | +* 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. |
0 commit comments