docs: add universal guide with production client app - with JK’s edits (#18707)

PR Close #18707
This commit is contained in:
Ward Bell 2017-10-06 14:34:29 -07:00 committed by Chuck Jazdzewski
parent 555b1cdf29
commit 0ea5f8b5ed
6 changed files with 70 additions and 149 deletions

View File

@ -0,0 +1,3 @@
{
"projectType": "systemjs"
}

View File

@ -23,7 +23,7 @@ server.engine('html', universalEngine({
appModuleFactory: AppServerModuleNgFactory appModuleFactory: AppServerModuleNgFactory
})); }));
// engine should find templates (like index-universal.html) in 'dist/' by default // engine should find templates in 'dist/' by default
server.set('views', 'dist'); server.set('views', 'dist');
// #enddocregion universal-engine // #enddocregion universal-engine

View File

@ -8,7 +8,6 @@ import { renderModuleFactory } from '@angular/platform-server';
import { APP_BASE_HREF } from '@angular/common'; import { APP_BASE_HREF } from '@angular/common';
const templateCache: { [key: string]: string } = {}; // page templates const templateCache: { [key: string]: string } = {}; // page templates
const outputCache: { [key: string]: string } = {}; // rendered pages
export function universalEngine(setupOptions: any) { export function universalEngine(setupOptions: any) {
@ -21,15 +20,6 @@ export function universalEngine(setupOptions: any) {
const { req } = options; const { req } = options;
const routeUrl = req.url; const routeUrl = req.url;
const html = outputCache[routeUrl];
if (html) {
// return already-built page for this url
console.log('from cache: ' + routeUrl);
callback(null, html);
return;
}
console.log('building: ' + routeUrl);
let template = templateCache[filePath]; let template = templateCache[filePath];
if (!template) { if (!template) {
template = fs.readFileSync(filePath).toString(); template = fs.readFileSync(filePath).toString();
@ -39,8 +29,8 @@ export function universalEngine(setupOptions: any) {
const { appModuleFactory } = setupOptions; const { appModuleFactory } = setupOptions;
const origin = getOrigin(req); const origin = getOrigin(req);
// render the page
// #docregion render // #docregion render
// render the page
renderModuleFactory(appModuleFactory, { renderModuleFactory(appModuleFactory, {
document: template, document: template,
url: routeUrl, url: routeUrl,
@ -48,10 +38,7 @@ export function universalEngine(setupOptions: any) {
{ provide: APP_BASE_HREF, useValue: origin } { provide: APP_BASE_HREF, useValue: origin }
] ]
}) })
.then(page => { .then(page => callback(null, page));
outputCache[routeUrl] = page;
callback(null, page);
});
// #enddocregion render // #enddocregion render
}; };
} }

View File

@ -12,7 +12,7 @@ module.exports = {
extensions: ['.ts', '.js'] extensions: ['.ts', '.js']
}, },
output: { output: {
path: 'dist', path: __dirname + '/dist',
filename: 'client.js' filename: 'client.js'
}, },
plugins: [ plugins: [

View File

@ -16,7 +16,7 @@ module.exports = {
}, },
target: 'node', target: 'node',
output: { output: {
path: 'dist', path: __dirname + '/dist',
filename: 'server.js' filename: 'server.js'
}, },
plugins: [ plugins: [
@ -28,7 +28,6 @@ module.exports = {
// copy assets to the output (/dist) folder // copy assets to the output (/dist) folder
new CopyWebpackPlugin([ new CopyWebpackPlugin([
{from: 'src/index-universal.html'}, {from: 'src/index-universal.html'},
{from: 'src/favicon.ico'},
{from: 'src/styles.css'}, {from: 'src/styles.css'},
{from: 'node_modules/core-js/client/shim.min.js'}, {from: 'node_modules/core-js/client/shim.min.js'},
{from: 'node_modules/zone.js/dist/zone.min.js'}, {from: 'node_modules/zone.js/dist/zone.min.js'},

View File

@ -2,6 +2,14 @@
This guide describes **Angular Universal**, a technology that runs your Angular application on the server. This guide describes **Angular Universal**, a technology that runs your Angular application on the server.
<div class="alert is-important">
This is a **preview guide**.
The Angular CLI is adding support for universal apps and
we will realign this guide with the CLI as soon as possible.
</div>
A normal Angular application executes in the _browser_, rendering pages in the DOM in response to user actions. A normal Angular application executes in the _browser_, rendering pages in the DOM in response to user actions.
**Angular Universal** generates _static_ application pages on the _server_ **Angular Universal** generates _static_ application pages on the _server_
@ -18,24 +26,8 @@ Meanwhile, the browser downloads the full client version and switches to it auto
[Download the finished sample code](generated/zips/universal/universal.zip), [Download the finished sample code](generated/zips/universal/universal.zip),
which runs in a [node express](https://expressjs.com/) server. which runs in a [node express](https://expressjs.com/) server.
Almost _any_ web server technology can serve a Universal app.
See this advanced example written for
[ASP.NET Core](https://github.com/MarkPieszak/aspnetcore-angular2-universal).
</div> </div>
<div class="alert is-important">
The build setup described in this guide is experimental and subject to change.
</div>
## Overview
This overview explains the benefits of a Universal application and how it works. Then it describes the sample application that goes with this guide.
Subsequent sections describe a sample Universal application derived from the Tour of Heroes tutorial
and explain how to build and run that app.
{@a why-do-it} {@a why-do-it}
### Why Universal ### Why Universal
@ -50,7 +42,7 @@ There are three main reasons to create a Universal version of your app.
{@a web-crawlers} {@a web-crawlers}
#### Facilitate web crawlers #### Facilitate web crawlers
Google, Bing, Facebook, twitter and other social media sites rely on web crawlers to index your application content and make that content searchable on the web. Google, Bing, Facebook, Twitter and other social media sites rely on web crawlers to index your application content and make that content searchable on the web.
These web crawlers may be unable to navigate and index your highly-interactive, Angular application as a human user could do. These web crawlers may be unable to navigate and index your highly-interactive, Angular application as a human user could do.
@ -75,10 +67,6 @@ people who otherwise would not be able to use the app at all.
Displaying the first page quickly can be critical for user engagement. Displaying the first page quickly can be critical for user engagement.
Captive users of a line-of-business app may have to wait.
But a casual visitor will switch to a faster site if your app takes "too long" to show the first page.
While [AOT](guide/aot-compiler) compilation speeds up application start times, it might not be fast enough for some of your audience, especially users on mobile devices with slow connections.
[53% of mobile site visits are abandoned](https://www.doubleclickbygoogle.com/articles/mobile-speed-matters/) if pages take longer than 3 seconds to load. [53% of mobile site visits are abandoned](https://www.doubleclickbygoogle.com/articles/mobile-speed-matters/) if pages take longer than 3 seconds to load.
Your app may have to launch faster to engage these users before they decide to do something else. Your app may have to launch faster to engage these users before they decide to do something else.
@ -86,19 +74,11 @@ With Angular Universal, you can generate landing pages for the app that look lik
The pages are pure HTML, and can display even if JavaScript is disabled. The pages are pure HTML, and can display even if JavaScript is disabled.
The pages do not handle browser events, but they _do_ support navigation through the site using [routerLink](guide/router.html#router-link). The pages do not handle browser events, but they _do_ support navigation through the site using [routerLink](guide/router.html#router-link).
Of course most Angular apps are highly interactive.
The landing page looks real and is far more useful than a "loading" spinner.
But it won't fool anyone for long.
In practice, you'll serve a static version of the landing page to hold the user's attention. In practice, you'll serve a static version of the landing page to hold the user's attention.
At the same time, you'll load the full Angular app behind it in the manner [explained below](#transition). At the same time, you'll load the full Angular app behind it in the manner [explained below](#transition).
The user perceives near-instant performance from the landing page The user perceives near-instant performance from the landing page
and gets the full interactive experience after the full app loads. and gets the full interactive experience after the full app loads.
<div class="l-sub-section">
Another tool called <a href="https://universal.angular.io/api/preboot/index.html">Preboot</a> can record browser events such as user keystrokes during the transition and play them back in the full Angular app once it is loaded.
</div>
{@a how-does-it-work} {@a how-does-it-work}
### How it works ### How it works
@ -124,7 +104,7 @@ Finally, the server returns the rendered page to the client.
### Working around the browser APIs ### Working around the browser APIs
Because a Universal `platform-server` app doesn't execute in the browser, you may have to work around some of the APIs and capabilities that you otherwise take for granted on the client. Because a Universal `platform-server` app doesn't execute in the browser, you may have to work around some of the browser APIs and capabilities that are missing on the server.
You won't be able reference browser-only native objects such as `window`, `document`, `navigator` or `location`. You won't be able reference browser-only native objects such as `window`, `document`, `navigator` or `location`.
If you don't need them on the server-rendered page, side-step them with conditional logic. If you don't need them on the server-rendered page, side-step them with conditional logic.
@ -137,11 +117,6 @@ Without mouse or keyboard events, a universal app can't rely on a user clicking
A universal app should determine what to render based solely on the incoming client request. A universal app should determine what to render based solely on the incoming client request.
This is a good argument for making the app [routeable](guide/router). This is a good argument for making the app [routeable](guide/router).
Http requests with _relative_ URLs don't work.
You should convert them to _absolute_ URLs on the server which means you'll need to know the server origin.
You can pass the server origin into your app with a [provider](guide/dependency-injection#injector-providers) "universal/*"
as you'll see in the [example below](#http-urls).
Because the user of a server-rendered page can't do much more than click links, Because the user of a server-rendered page can't do much more than click links,
you should [swap in the real client app](#transition) as quickly as possible for a proper interactive experience. you should [swap in the real client app](#transition) as quickly as possible for a proper interactive experience.
@ -224,27 +199,19 @@ npm install @angular/compiler-cli @angular/platform-server express --save
npm install webpack @ngtools/webpack copy-webpack-plugin raw-loader @types/express --save-dev npm install webpack @ngtools/webpack copy-webpack-plugin raw-loader @types/express --save-dev
</code-example> </code-example>
### Modify the client app
You'll have to modify the client application in a few small ways to enable server-side rendering and
to facilitate the transition from the Universal app to the client app.
{@a transition} {@a transition}
#### Enable transition to the client app ### Modify the client app
A Universal app can act as a dynamic "splash screen" that shows a view of your app while the real client app loads behind it. A Universal app can act as a dynamic, content-rich "splash screen" that engages the user.
This gives the appearance of a near-instant application. It gives the appearance of a near-instant application.
Meanwhile, the browser downloads the client app scripts in background. Meanwhile, the browser downloads the client app scripts in background.
Once loaded, Angular transitions from the static server-rendered page to the dynamically rendered views of the live client app. Once loaded, Angular transitions from the static server-rendered page to the dynamically rendered views of the interactive client app.
To make this work, the template for server-side rendering contains the `<script>` tags necessary to load the JavaScript and other asset files for the interactive client app. You must make a few changes to your application code to support both server-side rendering and the transition to the client app.
As is often the case, a production version of the client `index.html` acts as the template for server-side rendering. #### The root `AppModule`
You'll get to that [soon](#index-universal).
Essential changes to the root `AppModule` are the immediate concern.
Open file `src/app/app.module.ts` and find the `BrowserModule` import in the `NgModule` metadata. Open file `src/app/app.module.ts` and find the `BrowserModule` import in the `NgModule` metadata.
Replace that import with this one: Replace that import with this one:
@ -252,13 +219,7 @@ Replace that import with this one:
<code-example path="universal/src/app/app.module.ts" region="browsermodule" title="src/app/app.module.ts (withServerTransition)"> <code-example path="universal/src/app/app.module.ts" region="browsermodule" title="src/app/app.module.ts (withServerTransition)">
</code-example> </code-example>
<div class="l-sub-section"> Angular adds the `appId` value (which can be _any_ string) to the style-names of the server-rendered pages,
The `appId` can be any string. Keep it short.
</div>
Angular adds the `appId` to styles of the server-rendered pages,
so that they can be identified and removed when the client app starts. so that they can be identified and removed when the client app starts.
You can get runtime information about the current platform and the `appId` by injection. You can get runtime information about the current platform and the `appId` by injection.
@ -302,7 +263,7 @@ You will provide the `APP_BASE_HREF` in the universal version of the app (see ho
so the `heroesUrl` becomes absolute. so the `heroesUrl` becomes absolute.
Do the same thing to the `HttpSearchService` constructor. Do the same thing to the `HttpSearchService` constructor.
You'll have to tweak the `http.get` call in the `search()` method as well. You'll have to adjust the `http.get` call in the `search()` method as well.
Here's the revised class. Here's the revised class.
<code-example path="universal/src/app/hero-search.service.ts" region="class" title="src/app/hero-search.service.ts (with injected origin)" linenums="false"> <code-example path="universal/src/app/hero-search.service.ts" region="class" title="src/app/hero-search.service.ts (with injected origin)" linenums="false">
@ -322,30 +283,11 @@ When you are done, shut down the server with `ctrl-C`.
<div class="alert is-important"> <div class="alert is-important">
If you've been through this guide completely once before, If you get a "Cannot find module" error, see the explanation and resolution [below](#cannot-find-module)
the compiler may fail with the following error:
<code-example format="." language="bash">
error TS2307: Cannot find module '../../aot/src/universal/app-server.module.ngfactory'.
</code-example>
You must exclude the _server-side_ `/universal` folder files from _client app_ compilation.
Open `tsconfig.json`, find the `"exclude"` node and add `"universal/*"` to the array.
The result might look something like this:
```
"exclude": [
"node_modules/*",
"universal/*"
]
```
Compile and run again with `npm start`.
</div> </div>
<br><hr> <hr>
{@a server-code} {@a server-code}
@ -355,11 +297,11 @@ To run an Angular Universal application, you'll need a server that accepts clien
Create a `universal/` folder as a sibling to the `app/` folder. Create a `universal/` folder as a sibling to the `app/` folder.
Add to it the following three universal files: Add to it the following three universal parts:
1. The [app server module](#app-server-module) 1. the [app server module](#app-server-module)
2. The [Universal engine](#universal-engine) 2. the [Universal engine](#universal-engine)
3. The [web server](#web-server) 3. the [web server](#web-server)
{@a app-server-module} {@a app-server-module}
@ -376,8 +318,6 @@ Create an `app-server.module.ts` file in the `src/universal` directory with the
Notice that it imports first the client app's `AppModule` and then Angular Universal's `ServerModule`. Notice that it imports first the client app's `AppModule` and then Angular Universal's `ServerModule`.
This is also the place to register providers that are specific to running your app under Universal. This is also the place to register providers that are specific to running your app under Universal.
But don't register `APP_BASE_HREF` here;
register it in the [universal engine instead](#provide-origin).
{@a universal-engine} {@a universal-engine}
@ -394,13 +334,6 @@ Create a `universal-engine.ts` file in the `src/universal` directory with the fo
<code-example path="universal/src/universal/universal-engine.ts" title="src/universal/universal-engine.ts"> <code-example path="universal/src/universal/universal-engine.ts" title="src/universal/universal-engine.ts">
</code-example> </code-example>
<div class="alert is-helpful">
You can reuse this Universal template engine code in other applications.
It knows nothing about the tutorial sample application.
</div>
{@a render-module-factory} {@a render-module-factory}
#### Rendering the page #### Rendering the page
@ -426,30 +359,13 @@ Universal should render the appropriate page for that route.
{@a provide-origin} {@a provide-origin}
You supply `extraProviders` when your app needs information that can only be determined by the currently running server instance. You supply `extraProviders` when your app needs information that can only be determined by the currently running server instance.
The required information in this case is the running server's origin.
As [discussed above](#http-urls), HTTP request URLs must be absolute. The required information in this case is the running server's origin, provided under the `APP_BASE_HREF` token, so that the app can [calculate absolute HTTP URLs](#http-urls).
Our solution is to calculate the running server's origin (see `getOrigin()`) and provide it under the `APP_BASE_HREF` token.
#### Cached result
The `renderModuleFactory` function returns a _promise_ that resolves to the rendered page. The `renderModuleFactory` function returns a _promise_ that resolves to the rendered page.
It's up to your engine to decide what to do with that page. It's up to your engine to decide what to do with that page.
_This engine's_ promise callback returns the rendered page to the [web server](#web-server),
There are many alternatives.
For example, the rendered output could be stored as static HTML files to be served later.
This engine caches the rendered page in memory so that a given route is only rendered once.
<div class="alert is-important">
That's potentially simplistic.
Too many different requests could bloat server memory.
Volatile data on the page will get stale and could potentially mislead the user.
You might want to invalidate old cached pages periodically.
</div>
Most importantly, the promise callback returns the rendered page to the [web server](#web-server),
which then forwards it to the client in the HTTP response. which then forwards it to the client in the HTTP response.
{@a web-server} {@a web-server}
@ -458,7 +374,6 @@ which then forwards it to the client in the HTTP response.
A _Universal_ web server responds to application _page_ requests with static HTML rendered by the [Universal template engine](#universal-engine). A _Universal_ web server responds to application _page_ requests with static HTML rendered by the [Universal template engine](#universal-engine).
It's the same as any other web server in all other respects.
It receives and responds to HTTP requests from clients (usually browsers). It receives and responds to HTTP requests from clients (usually browsers).
It serves static assets such as scripts, css, and images. It serves static assets such as scripts, css, and images.
It may respond to data requests, perhaps directly or as a proxy to a separate data server. It may respond to data requests, perhaps directly or as a proxy to a separate data server.
@ -468,14 +383,11 @@ The sample web server for _this_ guide is based on the popular [Express](https:/
<div class="l-sub-section"> <div class="l-sub-section">
_Any_ web server technology can serve a Universal app as long as it can call Universal's `renderModuleFactory`. _Any_ web server technology can serve a Universal app as long as it can call Universal's `renderModuleFactory`.
The principles and decision points discussed below apply to any web server technology that you chose.
For example, see [this ASP.NET Core server](https://github.com/MarkPieszak/aspnetcore-angular2-universal)
Even if you've chosen a different server technology, the principles and decision points discussed below will apply to you.
</div> </div>
Create the `server.ts` file in the `src/universal` directory and add the following code: Create a `server.ts` file in the `src/universal` directory and add the following code:
<code-example path="universal/src/universal/server.ts" title="src/universal/server.ts"> <code-example path="universal/src/universal/server.ts" title="src/universal/server.ts">
</code-example> </code-example>
@ -493,7 +405,7 @@ just as you would for a normal Angular application server.
#### Import AppServerModule factory #### Import AppServerModule factory
Most of this server code is re-usable across many applications. Most of this server code is re-usable across many applications.
One line, the import of the `AppServerModule`, couples it specifically to a single application. The import of the `AppServerModule` couples it specifically to a single application.
<code-example path="universal/src/universal/server.ts" title="src/universal/server.ts" region="import-app-server-factory"> <code-example path="universal/src/universal/server.ts" title="src/universal/server.ts" region="import-app-server-factory">
</code-example> </code-example>
@ -501,10 +413,7 @@ One line, the import of the `AppServerModule`, couples it specifically to a sing
Your code editor may tell you that this import is incorrect. Your code editor may tell you that this import is incorrect.
It refers to the source file for the `AppServerModule` factory which doesn't exist at design time. It refers to the source file for the `AppServerModule` factory which doesn't exist at design time.
That file _will exist_, briefly, during compilation. That file _will exist_, briefly, during compilation. But it's never physically in the file system when you're editing `server.ts` and you must tell the compiler to generate this module factory file _before_ it compiles `server.ts`.
The build process creates it in the `../aot` directory, bundles it with other `universal/` code, and erases it during post-build cleanup. It's never around when you're editing `server.ts`.
All will be well as long as you arrange for the AOT compiler to generate this module factory file _before_ it compiles _this_ web server file.
[Learn how below](#universal-typescript-configuration). [Learn how below](#universal-typescript-configuration).
#### Add the Universal template engine #### Add the Universal template engine
@ -769,9 +678,8 @@ Open the browser's development tools.
In the console window you should see output like the following: In the console window you should see output like the following:
<code-example format="." language="bash" linenums="false"> <code-example format="." language="bash" linenums="false">
building: / listening on port 3200...
Running in the browser with appId=uni Running in the browser with appId=uni
/favicon.ico
/styles.css /styles.css
/shim.min.js /shim.min.js
/zone.min.js /zone.min.js
@ -779,12 +687,7 @@ Running in the browser with appId=uni
Error: ENOENT: no such file or directory, stat '... dist/client.js' ... Error: ENOENT: no such file or directory, stat '... dist/client.js' ...
</code-example> </code-example>
The first line shows that the server received a request for '/' and passed it to the Universal engine, which then built the HTML page from your Angular application. Most of the console log lines report requests for static files coming from the `<link>` and `<script>` tags in the `index-universal.html`.
Refresh the browser and the first line is `from cache: /` because your _universal template engine_
found the previously rendered page for `/` in its cache.
The remaining console log lines report requests for static files coming from the `<link>` and `<script>` tags in the `index-universal.html`.
The `.js` files in particular are needed to run the client version of the app in the browser. The `.js` files in particular are needed to run the client version of the app in the browser.
Once they're loaded, Angular _should_ replace the Universal-rendered page with the full client app. Once they're loaded, Angular _should_ replace the Universal-rendered page with the full client app.
@ -919,3 +822,32 @@ It also explained some of the key reasons for doing so.
Angular Universal can greatly improve the perceived startup performance of your app. Angular Universal can greatly improve the perceived startup performance of your app.
The slower the network, the more advantageous it becomes to have Universal display the first page to the user. The slower the network, the more advantageous it becomes to have Universal display the first page to the user.
{@a cannot-find-module}
#### Appendix: _Cannot find module_ error
As you continue to develop the application locally,
running the `npm start` command outside of universal, the compiler may fail with the following error:
<code-example format="." language="bash">
error TS2307: Cannot find module '../../aot/src/universal/app-server.module.ngfactory'.
</code-example>
The likely cause is that you've been through these guide steps before and now have a `/universal` folder.
That folder holds server-side artifacts that are irrelevant to the client app and are confusing the compiler.
You must exclude the _server-side_ `/universal` folder files from _client app_ compilation.
Open `tsconfig.json`, find the `"exclude"` node and add `"universal/*"` to the array.
The result might look something like this:
```
"exclude": [
"node_modules/*",
"universal/*"
]
```
Compile and run again with `npm start`.