Skip to content

Commit e9f541f

Browse files
MarkPieszakdond2clouds
authored andcommitted
docs: update documentation for universal (angular#7803)
1 parent b30d1ec commit e9f541f

File tree

1 file changed

+221
-39
lines changed

1 file changed

+221
-39
lines changed

docs/documentation/stories/universal-rendering.md

+221-39
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
1-
# Universal bundles
1+
# Angular Universal Integration
22

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.
3+
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.
44

5-
This story will show you how to set up Universal bundling for an existing `@angular/cli` project in 4 steps.
5+
---
66

7-
## Step 0: Install `@angular/platform-server`
7+
## Example CLI Integration:
8+
9+
[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.
10+
11+
---
12+
13+
# Integrating Angular Universal into existing CLI Applications
14+
15+
This story will show you how to set up Universal bundling for an existing `@angular/cli` project in 5 steps.
16+
17+
---
18+
19+
## Install Dependencies
820

921
Install `@angular/platform-server` into your project. Make sure you use the same version as the other `@angular` packages in your project.
1022

23+
> 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)
24+
1125
```bash
12-
$ npm install --save-dev @angular/platform-server
13-
```
14-
or
15-
```bash
16-
$ yarn add @angular/platform-server --dev
26+
$ npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader
1727
```
1828

19-
20-
## Step 1: Prepare your app for Universal rendering
29+
## Step 1: Prepare your App for Universal rendering
2130

2231
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:
2332

@@ -46,7 +55,7 @@ This example places it alongside `app.module.ts` in a file named `app.server.mod
4655

4756
### src/app/app.server.module.ts:
4857

49-
```javascript
58+
```typescript
5059
import {NgModule} from '@angular/core';
5160
import {ServerModule} from '@angular/platform-server';
5261

@@ -67,14 +76,15 @@ import {AppComponent} from './app.component';
6776
export class AppServerModule {}
6877
```
6978

70-
## Step 2: Create a server main file and tsconfig to build it
79+
---
80+
## Step 2: Create a server "main" file and tsconfig to build it
7181

7282
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`:
7383

7484
### src/main.server.ts:
7585

76-
```javascript
77-
export {AppServerModule} from './app/app.server.module';
86+
```typescript
87+
export { AppServerModule } from './app/app.server.module';
7888
```
7989

8090
Copy `tsconfig.app.json` to `tsconfig.server.json` and change it to build with a `"module"` target of `"commonjs"`.
@@ -105,6 +115,7 @@ Add a section for `"angularCompilerOptions"` and set `"entryModule"` to your `Ap
105115
}
106116
```
107117

118+
---
108119
## Step 3: Create a new project in `.angular-cli.json`
109120

110121
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 +129,17 @@ Then, remove the `"polyfills"` key - those aren't needed on the server, and adju
118129
...
119130
"apps": [
120131
{
121-
// Keep your original application config intact here.
122-
// It will be app 0.
132+
// Keep your original application config intact here, this is app 0
133+
// -EXCEPT- for outDir, udpate it to dist/browser
134+
"outDir": "dist/browser" // <-- update this
123135
},
124136
{
125137
// This is your server app. It is app 1.
126138
"platform": "server",
127139
"root": "src",
128-
// Build to dist-server instead of dist. This prevents
140+
// Build to dist/server instead of dist. This prevents
129141
// client and server builds from overwriting each other.
130-
"outDir": "dist-server",
142+
"outDir": "dist/server",
131143
"assets": [
132144
"assets",
133145
"favicon.ico"
@@ -163,41 +175,211 @@ Then, remove the `"polyfills"` key - those aren't needed on the server, and adju
163175
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`:
164176

165177
```bash
166-
# This builds the client application in dist/
178+
# This builds the client application in dist/browser/
167179
$ ng build --prod
168180
...
169-
# This builds the server bundle in dist-server/
170-
$ ng build --prod --app 1
181+
# This builds the server bundle in dist/server/
182+
$ ng build --prod --app 1 --output
183+
184+
# outputs:
171185
Date: 2017-07-24T22:42:09.739Z
172186
Hash: 9cac7d8e9434007fd8da
173187
Time: 4933ms
174188
chunk {0} main.988d7a161bd984b7eb54.bundle.js (main) 9.49 kB [entry] [rendered]
175189
chunk {1} styles.d41d8cd98f00b204e980.bundle.css (styles) 0 bytes [entry] [rendered]
176190
```
177191

178-
## Testing the bundle
192+
---
179193

180-
With this bundle built, you can use `renderModuleFactory` from `@angular/platform-server` to test it out.
181194

182-
```javascript
183-
// Load zone.js for the server.
184-
require('zone.js/dist/zone-node');
195+
## Step 4: Setting up an Express Server to run our Universal bundles
196+
197+
Now that we have everything set up to -make- the bundles, how we get everything running?
198+
199+
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.
200+
201+
```typescript
202+
app.engine('html', (_, options, callback) => {
203+
renderModuleFactory(AppServerModuleNgFactory, {
204+
// Our index.html
205+
document: template,
206+
url: options.req.url,
207+
// DI so that we can get lazy-loading to work differently (since we need it to just instantly render it)
208+
extraProviders: [
209+
provideModuleMap(LAZY_MODULE_MAP)
210+
]
211+
}).then(html => {
212+
callback(null, html);
213+
});
214+
});
215+
```
216+
217+
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.
218+
219+
```typescript
220+
// It's used as easily as
221+
import { ngExpressEngine } from '@nguniversal/express-engine';
222+
223+
app.engine('html', ngExpressEngine({
224+
bootstrap: AppServerModuleNgFactory,
225+
providers: [
226+
provideModuleMap(LAZY_MODULE_MAP)
227+
]
228+
}));
229+
```
230+
231+
Below we can see a TypeScript implementation of a -very- simple Express server to fire everything up.
232+
233+
> 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!
234+
235+
At the ROOT level of your project (where package.json / etc are), created a file named: **`server.ts`**
236+
237+
### ./server.ts (root project level)
238+
239+
```typescript
240+
// These are important and needed before anything else
241+
import 'zone.js/dist/zone-node';
242+
import 'reflect-metadata';
243+
244+
import { renderModuleFactory } from '@angular/platform-server';
245+
import { enableProdMode } from '@angular/core';
246+
247+
import * as express from 'express';
248+
import { join } from 'path';
249+
import { readFileSync } from 'fs';
250+
251+
// Faster server renders w/ Prod mode (dev mode never needed)
252+
enableProdMode();
253+
254+
// Express server
255+
const app = express();
256+
257+
const PORT = process.env.PORT || 4000;
258+
const DIST_FOLDER = join(process.cwd(), 'dist');
259+
260+
// Our index.html we'll use as our template
261+
const template = readFileSync(join(DIST_FOLDER, 'browser', 'index.html')).toString();
262+
263+
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
264+
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main.bundle');
185265

186-
// Import renderModuleFactory from @angular/platform-server.
187-
var renderModuleFactory = require('@angular/platform-server').renderModuleFactory;
266+
const { provideModuleMap } = require('@nguniversal/module-map-ngfactory-loader');
188267

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;
268+
app.engine('html', (_, options, callback) => {
269+
renderModuleFactory(AppServerModuleNgFactory, {
270+
// Our index.html
271+
document: template,
272+
url: options.req.url,
273+
// DI so that we can get lazy-loading to work differently (since we need it to just instantly render it)
274+
extraProviders: [
275+
provideModuleMap(LAZY_MODULE_MAP)
276+
]
277+
}).then(html => {
278+
callback(null, html);
279+
});
280+
});
192281

193-
// Load the index.html file.
194-
var index = require('fs').readFileSync('./src/index.html', 'utf8');
282+
app.set('view engine', 'html');
283+
app.set('views', join(DIST_FOLDER, 'browser'));
195284

196-
// Render to HTML and log it to the console.
197-
renderModuleFactory(AppServerModuleNgFactory, {document: index, url: '/'}).then(html => console.log(html));
285+
// Server static files from /browser
286+
app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));
287+
288+
// All regular routes use the Universal engine
289+
app.get('*', (req, res) => {
290+
res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req });
291+
});
292+
293+
// Start up the Node server
294+
app.listen(PORT, () => {
295+
console.log(`Node server listening on http://localhost:${PORT}`);
296+
});
297+
```
298+
299+
## Step 5: Setup a webpack config to handle this Node server.ts file and serve your application!
300+
301+
Now that we have our Node Express server setup, we need to pack it and serve it!
302+
303+
Create a file named `webpack.server.config.js` at the ROOT of your application.
304+
305+
> This file basically takes that server.ts file, and takes it and compiles it and every dependency it has into `dist/server.js`.
306+
307+
### ./webpack.server.config.js (root project level)
308+
309+
```typescript
310+
const path = require('path');
311+
const webpack = require('webpack');
312+
313+
module.exports = {
314+
entry: { server: './server.ts' },
315+
resolve: { extensions: ['.ts', '.js'] },
316+
target: 'node',
317+
// this makes sure we include node_modules and other 3rd party libraries
318+
externals: [/(node_modules|main\..*\.js)/],
319+
output: {
320+
path: path.join(__dirname, 'dist'),
321+
filename: '[name].js'
322+
},
323+
module: {
324+
rules: [
325+
{ test: /\.ts$/, loader: 'ts-loader' }
326+
]
327+
},
328+
plugins: [
329+
// Temporary Fix for issue: https://github.com/angular/angular/issues/11580
330+
// for "WARNING Critical dependency: the request of a dependency is an expression"
331+
new webpack.ContextReplacementPlugin(
332+
/(.+)?angular(\\|\/)core(.+)?/,
333+
path.join(__dirname, 'src'), // location of your src
334+
{} // a map of your routes
335+
),
336+
new webpack.ContextReplacementPlugin(
337+
/(.+)?express(\\|\/)(.+)?/,
338+
path.join(__dirname, 'src'),
339+
)
340+
]
341+
}
342+
```
343+
344+
**Almost there!**
345+
346+
Now let's see what our resulting structure should look like, if we open up our `/dist/` folder we should see:
347+
348+
```
349+
/dist/
350+
/browser/
351+
/server/
352+
```
353+
354+
To fire up the application, in your terminal enter
355+
356+
```bash
357+
node dist/server.js
358+
```
359+
360+
:sparkles:
361+
362+
Now lets create a few handy scripts to help us do all of this in the future.
363+
364+
```json
365+
"scripts": {
366+
367+
// These will be your common scripts
368+
"build:dynamic": "npm run build:client-and-server-bundles && npm run webpack:server",
369+
"serve:dynamic": "node dist/server.js",
370+
371+
// Helpers for the above scripts
372+
"build:client-and-server-bundles": "ng build --prod && ng build --prod --app 1 --output-hashing=false",
373+
"webpack:server": "webpack --config webpack.server.config.js --progress --colors"
374+
}
375+
```
376+
377+
In the future when you want to see a Production build of your app with Universal (locally), you can simply run:
378+
379+
```bash
380+
npm run build:dynamic && npm run serve:dynamic
198381
```
199382

200-
## Caveats
383+
Enjoy!
201384

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.
385+
Once again to see a working version of everything, check out the [universal-starter](https://github.com/angular/universal-starter/tree/master/cli).

0 commit comments

Comments
 (0)