Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: update documentation for universal #7796

Closed
wants to merge 9 commits into from
247 changes: 215 additions & 32 deletions docs/documentation/stories/universal-rendering.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
# 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.
---

## 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.

---

## 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
```


Expand Down Expand Up @@ -46,7 +58,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';

Expand All @@ -67,13 +79,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';
```

Expand Down Expand Up @@ -105,6 +118,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"`.
Expand All @@ -118,16 +132,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"
Expand Down Expand Up @@ -163,41 +178,209 @@ 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
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';

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`**

```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();

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');

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should include installation of this package in the installation section.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


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')));

// Import renderModuleFactory from @angular/platform-server.
var renderModuleFactory = require('@angular/platform-server').renderModuleFactory;
// ALl regular routes use the Universal engine

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ALl typo

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

app.get('*', (req, res) => {
res.render('index', { req });

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes you're running the server.js in the same folder as your index.html yet below we describe using dist/browser, maybe sync those up?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need directory stuff here, we are just calling the engine here

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant the .render('index' dosen't that look for index.html in the directory it's being executed in?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It has reference to it, since it's a lookup of the View, so having just index alone works since it knows which oneto find. You can be explicit and do res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req }); but there's no need. ExpressEngines are almost always just a string like this one.

});

// 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;
// Start up the Node server
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});
```

## Step 5: Setup a webpack config to handle this Node server.ts file and serve your application!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mention that this is work around for #7200 ?
It makes sense for the dynamic case since you shouldn't have to do a npm install on your server, but for static not so much (if #7200 wasn't an issue)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I'll put that in there, good idea! Forgot the issue number 👍


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`.

// Load the index.html file.
var index = require('fs').readFileSync('./src/index.html', 'utf8');
```typescript
const path = require('path');

// Render to HTML and log it to the console.
renderModuleFactory(AppServerModuleNgFactory, {document: index, url: '/'}).then(html => console.log(html));
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
```

## Caveats
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)

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.
- 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)