docs: add universal guide with production client app - with JK’s edits (#18707)
PR Close #18707
This commit is contained in:
parent
555b1cdf29
commit
0ea5f8b5ed
@ -0,0 +1,3 @@
|
||||
{
|
||||
"projectType": "systemjs"
|
||||
}
|
@ -23,7 +23,7 @@ server.engine('html', universalEngine({
|
||||
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');
|
||||
// #enddocregion universal-engine
|
||||
|
||||
|
@ -8,7 +8,6 @@ import { renderModuleFactory } from '@angular/platform-server';
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
|
||||
const templateCache: { [key: string]: string } = {}; // page templates
|
||||
const outputCache: { [key: string]: string } = {}; // rendered pages
|
||||
|
||||
export function universalEngine(setupOptions: any) {
|
||||
|
||||
@ -21,15 +20,6 @@ export function universalEngine(setupOptions: any) {
|
||||
const { req } = options;
|
||||
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];
|
||||
if (!template) {
|
||||
template = fs.readFileSync(filePath).toString();
|
||||
@ -39,8 +29,8 @@ export function universalEngine(setupOptions: any) {
|
||||
const { appModuleFactory } = setupOptions;
|
||||
const origin = getOrigin(req);
|
||||
|
||||
// render the page
|
||||
// #docregion render
|
||||
// render the page
|
||||
renderModuleFactory(appModuleFactory, {
|
||||
document: template,
|
||||
url: routeUrl,
|
||||
@ -48,10 +38,7 @@ export function universalEngine(setupOptions: any) {
|
||||
{ provide: APP_BASE_HREF, useValue: origin }
|
||||
]
|
||||
})
|
||||
.then(page => {
|
||||
outputCache[routeUrl] = page;
|
||||
callback(null, page);
|
||||
});
|
||||
.then(page => callback(null, page));
|
||||
// #enddocregion render
|
||||
};
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ module.exports = {
|
||||
extensions: ['.ts', '.js']
|
||||
},
|
||||
output: {
|
||||
path: 'dist',
|
||||
path: __dirname + '/dist',
|
||||
filename: 'client.js'
|
||||
},
|
||||
plugins: [
|
||||
|
@ -16,7 +16,7 @@ module.exports = {
|
||||
},
|
||||
target: 'node',
|
||||
output: {
|
||||
path: 'dist',
|
||||
path: __dirname + '/dist',
|
||||
filename: 'server.js'
|
||||
},
|
||||
plugins: [
|
||||
@ -28,7 +28,6 @@ module.exports = {
|
||||
// copy assets to the output (/dist) folder
|
||||
new CopyWebpackPlugin([
|
||||
{from: 'src/index-universal.html'},
|
||||
{from: 'src/favicon.ico'},
|
||||
{from: 'src/styles.css'},
|
||||
{from: 'node_modules/core-js/client/shim.min.js'},
|
||||
{from: 'node_modules/zone.js/dist/zone.min.js'},
|
||||
|
@ -2,6 +2,14 @@
|
||||
|
||||
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.
|
||||
|
||||
**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),
|
||||
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 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}
|
||||
|
||||
### Why Universal
|
||||
@ -50,7 +42,7 @@ There are three main reasons to create a Universal version of your app.
|
||||
{@a 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.
|
||||
|
||||
@ -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.
|
||||
|
||||
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.
|
||||
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 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.
|
||||
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
|
||||
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}
|
||||
### How it works
|
||||
|
||||
@ -124,7 +104,7 @@ Finally, the server returns the rendered page to the client.
|
||||
|
||||
### 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`.
|
||||
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.
|
||||
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,
|
||||
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
|
||||
</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}
|
||||
|
||||
#### 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.
|
||||
This gives the appearance of a near-instant application.
|
||||
A Universal app can act as a dynamic, content-rich "splash screen" that engages the user.
|
||||
It gives the appearance of a near-instant application.
|
||||
|
||||
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.
|
||||
You'll get to that [soon](#index-universal).
|
||||
|
||||
Essential changes to the root `AppModule` are the immediate concern.
|
||||
#### The root `AppModule`
|
||||
|
||||
Open file `src/app/app.module.ts` and find the `BrowserModule` import in the `NgModule` metadata.
|
||||
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>
|
||||
|
||||
<div class="l-sub-section">
|
||||
|
||||
The `appId` can be any string. Keep it short.
|
||||
|
||||
</div>
|
||||
|
||||
Angular adds the `appId` to styles of the server-rendered pages,
|
||||
Angular adds the `appId` value (which can be _any_ string) to the style-names of the server-rendered pages,
|
||||
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.
|
||||
@ -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.
|
||||
|
||||
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.
|
||||
|
||||
<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">
|
||||
|
||||
If you've been through this guide completely once before,
|
||||
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`.
|
||||
If you get a "Cannot find module" error, see the explanation and resolution [below](#cannot-find-module)
|
||||
|
||||
</div>
|
||||
|
||||
<br><hr>
|
||||
<hr>
|
||||
|
||||
{@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.
|
||||
|
||||
Add to it the following three universal files:
|
||||
Add to it the following three universal parts:
|
||||
|
||||
1. The [app server module](#app-server-module)
|
||||
2. The [Universal engine](#universal-engine)
|
||||
3. The [web server](#web-server)
|
||||
1. the [app server module](#app-server-module)
|
||||
2. the [Universal engine](#universal-engine)
|
||||
3. the [web server](#web-server)
|
||||
|
||||
{@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`.
|
||||
|
||||
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}
|
||||
|
||||
@ -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>
|
||||
|
||||
<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}
|
||||
|
||||
#### Rendering the page
|
||||
@ -426,30 +359,13 @@ Universal should render the appropriate page for that route.
|
||||
{@a provide-origin}
|
||||
|
||||
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.
|
||||
Our solution is to calculate the running server's origin (see `getOrigin()`) and provide it under the `APP_BASE_HREF` token.
|
||||
|
||||
#### Cached result
|
||||
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).
|
||||
|
||||
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.
|
||||
|
||||
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),
|
||||
_This engine's_ promise callback returns the rendered page to the [web server](#web-server),
|
||||
which then forwards it to the client in the HTTP response.
|
||||
|
||||
{@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).
|
||||
|
||||
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 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.
|
||||
@ -468,14 +383,11 @@ The sample web server for _this_ guide is based on the popular [Express](https:/
|
||||
<div class="l-sub-section">
|
||||
|
||||
_Any_ web server technology can serve a Universal app as long as it can call Universal's `renderModuleFactory`.
|
||||
|
||||
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.
|
||||
The principles and decision points discussed below apply to any web server technology that you chose.
|
||||
|
||||
</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>
|
||||
@ -493,7 +405,7 @@ just as you would for a normal Angular application server.
|
||||
#### Import AppServerModule factory
|
||||
|
||||
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>
|
||||
@ -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.
|
||||
It refers to the source file for the `AppServerModule` factory which doesn't exist at design time.
|
||||
|
||||
That file _will exist_, briefly, during compilation.
|
||||
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.
|
||||
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`.
|
||||
[Learn how below](#universal-typescript-configuration).
|
||||
|
||||
#### 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:
|
||||
|
||||
<code-example format="." language="bash" linenums="false">
|
||||
building: /
|
||||
listening on port 3200...
|
||||
Running in the browser with appId=uni
|
||||
/favicon.ico
|
||||
/styles.css
|
||||
/shim.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' ...
|
||||
</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.
|
||||
|
||||
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`.
|
||||
Most of the 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.
|
||||
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.
|
||||
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`.
|
Loading…
x
Reference in New Issue
Block a user