填寫這份《一分鐘調查》,幫我們(開發組)做得更好!去填寫Home

Angular Universal:Angular 統一平臺簡介

Server-side rendering (SSR) with Angular Universal

本指南講的是Angular Universal(統一平臺),一項在伺服器端執行 Angular 應用的技術。

This guide describes Angular Universal, a technology that renders Angular applications on the server.

標準的 Angular 應用會執行在瀏覽器中,它會在 DOM 中渲染頁面,以響應使用者的操作。 而Angular Universal 會在伺服器端執行,產生一些靜態的應用頁面,稍後再透過客戶端進行啟動。 這意味著該應用的渲染通常會更快,讓使用者可以在應用變得完全可互動之前,先檢視應用的佈局。

A normal Angular application executes in the browser, rendering pages in the DOM in response to user actions. Angular Universal executes on the server, generating static application pages that later get bootstrapped on the client. This means that the application generally renders more quickly, giving users a chance to view the application layout before it becomes fully interactive.

要了解 SSR 的其它技術和概念的詳細資訊,請參閱這篇文章

For a more detailed look at different techniques and concepts surrounding SSR, please check out this article.

你可以使用 Angular CLI 來輕鬆為應用做好伺服器端渲染的準備。CLI 的 @nguniversal/express-engine 範本會執行下面所講的必要步驟。

You can easily prepare an app for server-side rendering using the Angular CLI. The CLI schematic @nguniversal/express-engine performs the required steps, as described below.

注意:下載已完成的範例程式碼下載已完成的範例程式碼,並將其執行在一個 Node.js® Express 伺服器中。

Note:Download the finished sample codeDownload the finished sample code, which runs in a Node.js® Express server.

Universal 課程

Universal tutorial

這次演練的基礎是“英雄之旅”課程

The Tour of Heroes tutorial is the foundation for this walkthrough.

在這個例子中,Angular CLI 使用 預先(AoT)編譯器編譯並打包了該應用的 Universal 版本。Node.js Express Web 伺服器則會根據客戶端的請求,利用 Universal 編譯 HTML 頁面。

In this example, the Angular CLI compiles and bundles the Universal version of the app with the Ahead-of-Time (AOT) compiler. A Node.js Express web server compiles HTML pages with Universal based on client requests.

要建立伺服器端應用模組 app.server.module.ts,請執行以下 CLI 命令。

To create the server-side app module, app.server.module.ts, run the following CLI command.

ng add @nguniversal/express-engine
      
      ng add @nguniversal/express-engine
    

該命令會建立如下資料夾結構。

The command creates the following folder structure.

src/ index.html app web page main.ts bootstrapper for client app main.server.ts * bootstrapper for server app style.css styles for the app app/ ... application code app.server.module.ts * server-side application module server.ts * express web server tsconfig.json TypeScript base configuration tsconfig.app.json TypeScript browser application configuration tsconfig.server.json TypeScript server application configuration tsconfig.spec.json TypeScript tests configuration
      
      src/
  index.html                 app web page
  main.ts                    bootstrapper for client app
  main.server.ts             * bootstrapper for server app
  style.css                  styles for the app
  app/ ...                   application code
    app.server.module.ts     * server-side application module
server.ts                    * express web server
tsconfig.json                TypeScript base configuration
tsconfig.app.json            TypeScript browser application configuration
tsconfig.server.json         TypeScript server application configuration
tsconfig.spec.json           TypeScript tests configuration
    

標有 * 的檔案都是新增的,不在原始的課程範例中。

The files marked with * are new and not in the original tutorial sample.

Universal 實戰

Universal in action

要使用 Universal 在本地系統中渲染你的應用,請使用如下命令。

To start rendering your app with Universal on your local system, use the following command.

npm run dev:ssr
      
      npm run dev:ssr
    

開啟瀏覽器,導航到 http://localhost:4200/。你會看到熟悉的“英雄之旅”儀表盤頁面。

Open a browser and navigate to http://localhost:4200/. You should see the familiar Tour of Heroes dashboard page.

透過 routerLinks 導航時能正常工作,因為它們使用的是原生的連結標籤(<a>)。你可以從儀表盤進入 英雄列表頁面,然後返回。你可以點選儀表盤頁面上的一個英雄來顯示他的詳情頁面。

Navigation via routerLinks works correctly because they use the native anchor (<a>) tags. You can go from the Dashboard to the Heroes page and back. You can click a hero on the Dashboard page to display its Details page.

如果你限制下網速(稍後會講操作步驟),讓客戶端指令碼下載時間變長,你會注意到:

If you throttle your network speed so that the client-side scripts take longer to download (instructions below), you'll notice:

  • 點選英雄列表頁面上的英雄沒有反應。

    Clicking a hero on the Heroes page does nothing.

  • 你無法新增或刪除英雄。

    You can't add or delete a hero.

  • 儀表盤頁面上的搜尋框會被忽略。

    The search box on the Dashboard page is ignored.

  • “詳情”頁面上的後退儲存按鈕不起作用。

    The Back and Save buttons on the Details page don't work.

不支援除了點選 routerLink 以外的任何使用者事件。你必須等待完整的客戶端應用啟動並執行,或者使用 preboot 之類別的函式庫來緩衝這些事件,這樣你就可以在客戶端指令碼載入完畢後重放這些事件。

User events other than routerLink clicks aren't supported. You must wait for the full client app to bootstrap and run, or buffer the events using libraries like preboot, which allow you to replay these events once the client-side scripts load.

在開發機器上,從伺服器端渲染的應用過渡到客戶端應用的過程會很快,但是你還是應該在實際場景中測試一下你的應用。

The transition from the server-rendered app to the client app happens quickly on a development machine, but you should always test your apps in real-world scenarios.

你可以透過模擬速度較慢的網路來更清晰地看到這種轉換,如下所示:

You can simulate a slower network to see the transition more clearly as follows:

  1. 開啟 Chrome 開發者工具,進入 Network 標籤頁。

    Open the Chrome Dev Tools and go to the Network tab.

  2. 找一下選單欄最右側的 Network Throttling 下拉選單。

    Find the Network Throttling dropdown on the far right of the menu bar.

  3. 嘗試一下 “3G” 的速度吧。

    Try one of the "3G" speeds.

伺服器端渲染的應用仍然可以快速啟動,但完整的客戶端應用可能需要幾秒鐘才能載入完。

The server-rendered app still launches quickly but the full client app may take seconds to load.

為何需要伺服器端渲染?

Why use server-side rendering?

有三個主要的理由來為你的應用建立一個 Universal 版本。

There are three main reasons to create a Universal version of your app.

  1. 透過搜尋引擎優化(SEO)來幫助網路爬蟲。

    Facilitate web crawlers through search engine optimization (SEO)

  2. 提升在手機和低功耗裝置上的效能

    Improve performance on mobile and low-powered devices

  3. 迅速顯示出第一個支援首次內容繪製(FCP)的頁面

    Show the first page quickly with a first-contentful paint (FCP)

幫助網路爬蟲(SEO)

Facilitate web crawlers (SEO)

Google、Bing、Facebook、Twitter 和其它社交媒體網站都依賴網路爬蟲去索引你的應用內容,並且讓它的內容可以透過網路搜尋到。

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.

這些網路爬蟲可能不會像人類那樣導航到你的具有高度互動性的 Angular 應用,並為其建立索引。

These web crawlers may be unable to navigate and index your highly interactive Angular application as a human user could do.

Angular Universal 可以為你產生應用的靜態版本,它易搜尋、可連結,瀏覽時也不必藉助 JavaScript。 它也讓站點可以被預覽,因為每個 URL 返回的都是一個完全渲染好的頁面。

Angular Universal can generate a static version of your app that is easily searchable, linkable, and navigable without JavaScript. Universal also makes a site preview available since each URL returns a fully rendered page.

提升手機和低功耗裝置上的效能

Improve performance on mobile and low-powered devices

有些裝置不支援 JavaScript 或 JavaScript 執行得很差,導致使用者體驗不可接受。 對於這些情況,你可能會需要該應用的伺服器端渲染的、無 JavaScript 的版本。 雖然有一些限制,不過這個版本可能是那些完全沒辦法使用該應用的人的唯一選擇。

Some devices don't support JavaScript or execute JavaScript so poorly that the user experience is unacceptable. For these cases, you may require a server-rendered, no-JavaScript version of the app. This version, however limited, may be the only practical alternative for people who otherwise couldn't use the app at all.

快速顯示第一頁

Show the first page quickly

快速顯示第一頁對於吸參考戶是至關重要的。 載入速度更快的頁面效果更好,即使其差異只有 100 毫秒也是如此(https://web.dev/shopping-for-speed-on-ebay/)。 你的應用要啟動得更快一點,以便在使用者決定做別的事情之前吸引他們的注意力。

Displaying the first page quickly can be critical for user engagement. Pages that load faster perform better, even with changes as small as 100ms. Your app may have to launch faster to engage these users before they decide to do something else.

使用 Angular Universal,你可以為應用產生“著陸頁”,它們看起來就和完整的應用一樣。 這些著陸頁是純 HTML,並且即使 JavaScript 被禁用了也能顯示。 這些頁面不會處理瀏覽器事件,不過它們可以[routerLink](guide/router#router-link) 在這個網站中導航。

With Angular Universal, you can generate landing pages for the app that look like the complete app. The pages are pure HTML, and can display even if JavaScript is disabled. The pages don't handle browser events, but they do support navigation through the site using routerLink.

在實踐中,你可能要使用一個著陸頁的靜態版本來保持使用者的注意力。 同時,你也會在幕後載入完整的 Angular 應用。 使用者會覺得著陸頁幾乎是立即出現的,而當完整的應用載入完之後,又可以獲得完整的互動體驗。

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. The user perceives near-instant performance from the landing page and gets the full interactive experience after the full app loads.

Universal Web 伺服器

Universal web servers

Universal Web 伺服器使用 Universal 範本引擎渲染出的靜態 HTML 來響應對應用頁面的請求。 伺服器接收並響應來自客戶端(通常是瀏覽器)的 HTTP 請求,並回復靜態檔案,如指令碼、CSS 和圖片。 它可以直接響應資料請求,也可以作為獨立資料伺服器的代理進行響應。

A Universal web server responds to application page requests with static HTML rendered by the Universal template engine. The server receives and responds to HTTP requests from clients (usually browsers), and serves static assets such as scripts, CSS, and images. It may respond to data requests, either directly or as a proxy to a separate data server.

這個例子中的範例 Web 伺服器是基於常見的 Express 框架的。

The sample web server for this guide is based on the popular Express framework.

注意: 任何一種 Web 伺服器技術都可以作為 Universal 應用的伺服器,只要它能呼叫 Universal 的 renderModule() 函式。 這裡所討論的這些原則和決策點也適用於任何 Web 伺服器技術。

Note: Any web server technology can serve a Universal app as long as it can call Universal's renderModule() function. The principles and decision points discussed here apply to any web server technology.

Universal 應用使用 platform-server 包(而不是 platform-browser),它提供了 DOM 的伺服器端實現、XMLHttpRequest 以及其它不依賴瀏覽器的底層特性。

Universal applications use the Angular platform-server package (as opposed to platform-browser), which provides server implementations of the DOM, XMLHttpRequest, and other low-level features that don't rely on a browser.

伺服器(這個例子中使用的是 Node.js Express 伺服器)會把客戶端對應用頁面的請求傳給 NgUniversal 的 ngExpressEngine。在內部實現上,它會呼叫 Universal 的 renderModule() 函式,它還提供了快取等有用的工具函式。

The server (Node.js Express in this guide's example) passes client requests for application pages to the NgUniversal ngExpressEngine. Under the hood, this calls Universal's renderModule() function, while providing caching and other helpful utilities.

renderModule() 函式接受一個範本 HTML 頁面(通常是 index.html)、一個包含元件的 Angular 模組和一個用於決定該顯示哪些元件的路由作為輸入。

The renderModule() function takes as inputs a template HTML page (usually index.html), an Angular module containing components, and a route that determines which components to display.

該路由從客戶端的請求中傳給伺服器。

The route comes from the client's request to the server.

每次請求都會給出所請求路由的一個適當的檢視。

Each request results in the appropriate view for the requested route.

renderModule() 在範本中的 <app> 標記中渲染出這個檢視,並為客戶端建立一個完成的 HTML 頁面。

The renderModule() function renders the view within the <app> tag of the template, creating a finished HTML page for the client.

最後,伺服器就會把渲染好的頁面返回給客戶端。

Finally, the server returns the rendered page to the client.

使用瀏覽器 API

Working around the browser APIs

由於 Universal 應用並沒有執行在瀏覽器中,因此該伺服器上可能會缺少瀏覽器的某些 API 和其它能力。

Because a Universal app doesn't execute in the browser, some of the browser APIs and capabilities may be missing on the server.

比如,伺服器端應用不能參考瀏覽器獨有的全域性物件,比如 windowdocumentnavigatorlocation

For example, server-side applications can't reference browser-only global objects such as window, document, navigator, or location.

Angular 提供了一些這些物件的可注入的抽象層,比如 LocationDOCUMENT,它可以作為你所呼叫的 API 的等效替身。 如果 Angular 沒有提供它,你也可以寫一個自己的抽象層,當在瀏覽器中執行時,就把它委託給瀏覽器 API,當它在伺服器中執行時,就提供一個符合要求的代用實現(也叫墊片 - shimming)。

Angular provides some injectable abstractions over these objects, such as Locationor DOCUMENT; it may substitute adequately for these APIs. If Angular doesn't provide it, it's possible to write new abstractions that delegate to the browser APIs while in the browser and to an alternative implementation while on the server (aka shimming).

同樣,由於沒有滑鼠或鍵盤事件,因此 Universal 應用也不能依賴於使用者點選某個按鈕來顯示每個元件。 Universal 應用必須僅僅根據客戶端過來的請求決定要渲染的內容。 把該應用做成可路由的,就是一種好方案。

Similarly, without mouse or keyboard events, a server-side app can't rely on a user clicking a button to show a component. The app must determine what to render based solely on the incoming client request. This is a good argument for making the app routable.

Universal 範本引擎

Universal template engine

server.ts 檔案中最重要的部分是 ngExpressEngine() 函式:

The important bit in the server.ts file is the ngExpressEngine() function.

// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) server.engine('html', ngExpressEngine({ bootstrap: AppServerModule, }));
server.ts
      
      // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
server.engine('html', ngExpressEngine({
  bootstrap: AppServerModule,
}));
    

ngExpressEngine() 是對 Universal 的 renderModule() 函式的封裝。它會把客戶端請求轉換成伺服器端渲染的 HTML 頁面。 它接受一個具有下列屬性的物件:

The ngExpressEngine() function is a wrapper around Universal's renderModule() function which turns a client's requests into server-rendered HTML pages. It accepts an object with the following properties:

  • bootstrap:在伺服器上渲染時用於引導應用程式的根 NgModuleNgModule 工廠。對於這個範例應用,它是 AppServerModule。它是 Universal 伺服器端渲染器和 Angular 應用之間的橋樑。

    bootstrap: The root NgModule or NgModule factory to use for bootstraping the app when rendering on the server. For the example app, it is AppServerModule. It's the bridge between the Universal server-side renderer and the Angular application.

  • extraProviders:這是可選的,可以讓你指定僅在伺服器渲染應用程式時才適用的依賴提供者。當你的應用需要某些只能由當前執行的伺服器實例確定的資訊時,可以執行此操作。

    extraProviders: This is optional and lets you specify dependency providers that apply only when rendering the app on the server. You can do this when your app needs information that can only be determined by the currently running server instance.

ngExpressEngine() 函式返回了一個會解析成渲染好的頁面的承諾(Promise)。 接下來你的引擎要決定拿這個頁面做點什麼。 在這個引擎Promise 回呼(Callback)函式中,把渲染好的頁面返回給了 Web 伺服器,然後伺服器透過 HTTP 響應把它轉發給了客戶端。

The ngExpressEngine() function returns a Promise callback that resolves to the rendered page. It's up to the engine to decide what to do with that page. This engine's Promise callback returns the rendered page to the web server, which then forwards it to the client in the HTTP response.

注意: 這個包裝器幫助隱藏了 renderModule() 的複雜性。 在 Universal 程式碼函式庫中還有更多針對其它後端技術的包裝器。

Note: These wrappers help hide the complexity of the renderModule() function. There are more wrappers for different backend technologies at the Universal repository.

過濾請求的 URL

Filtering request URLs

注意:當使用 NgUniversal Express 原理圖時,將自動處理稍後描述的基本行為。當你要嘗試理解其底層行為或在不使用原理圖的情況下自行實現它時,這一節會很有用。

NOTE: The basic behavior described below is handled automatically when using the NgUniversal Express schematic. This is helpful when trying to understand the underlying behavior or replicate it without using the schematic.

Web 伺服器必須把對應用頁面的請求和其它型別的請求區分開。

The web server must distinguish app page requests from other kinds of requests.

這可不像攔截對根路徑 / 的請求那麼簡單。 瀏覽器可以請求應用中的任何一個路由地址,比如 /dashboard/heroes/detail:12。 事實上,如果應用會透過伺服器渲染,那麼應用中點選的任何一個連結都會發到伺服器,就像導航時的地址會發到路由器一樣。

It's not as simple as intercepting a request to the root address /. The browser could ask for one of the application routes such as /dashboard, /heroes, or /detail:12. In fact, if the app were only rendered by the server, every app link clicked would arrive at the server as a navigation URL intended for the router.

幸運的是,應用的路由具有一些共同特徵:它們的 URL 一般不帶副檔名。 (資料請求也可能缺少副檔名,但是它們很容易識別出來,因為它們總是以 /api 開頭,所有的靜態資源的請求都會帶有一個副檔名,比如 main.js/node_modules/zone.js/dist/zone.js)。

Fortunately, application routes have something in common: their URLs lack file extensions. (Data requests also lack extensions but they're easy to recognize because they always begin with /api.) All static asset requests have a file extension (such as main.js or /node_modules/zone.js/bundles/zone.umd.js).

由於使用了路由,所以我們可以輕鬆的識別出這三種類型的請求,並分別處理它們。

Because we use routing, we can easily recognize the three types of requests and handle them differently.

  1. 資料請求:請求的 URL 用 /api 開頭

    Data request: request URL that begins /api.

  2. 應用導航:請求的 URL 不帶副檔名

    App navigation: request URL with no file extension.

  3. 靜態資源:所有其它請求。

    Static asset: all other requests.

Node.js Express 伺服器是一系列中介軟體構成的管道,它會挨個對 URL 請求進行過濾和處理。 你可以呼叫 app.get() 來配置 Express 伺服器的管道,就像下面這個資料請求一樣:

A Node.js Express server is a pipeline of middleware that filters and processes requests one after the other. You configure the Node.js Express server pipeline with calls to server.get() like this one for data requests.

// TODO: implement data requests securely server.get('/api/**', (req, res) => { res.status(404).send('data requests are not yet supported'); });
server.ts (data URL)
      
      // TODO: implement data requests securely
server.get('/api/**', (req, res) => {
  res.status(404).send('data requests are not yet supported');
});
    

注意:這個範例伺服器不會處理資料請求。

Note: This sample server doesn't handle data requests.

本課程的“記憶體 Web API” 模組(一個示範及開發工具)攔截了所有 HTTP 呼叫,並且模擬了遠端資料伺服器的行為。 在實踐中,你應該移除這個模組,並且在伺服器上註冊你的 Web API 中介軟體。

The tutorial's "in-memory web API" module, a demo and development tool, intercepts all HTTP calls and simulates the behavior of a remote data server. In practice, you would remove that module and register your web API middleware on the server here.

下列程式碼會過濾出不帶副檔名的 URL,並把它們當做導航請求進行處理。

The following code filters for request URLs with no extensions and treats them as navigation requests.

// All regular routes use the Universal engine server.get('*', (req, res) => { res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] }); });
server.ts (navigation)
      
      // All regular routes use the Universal engine
server.get('*', (req, res) => {
  res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});
    

安全的提供靜態檔案

Serving static files safely

單獨的 server.use() 會處理所有其它 URL,比如對 JavaScript 、圖片和樣式表等靜態資源的請求。

A single server.use() treats all other URLs as requests for static assets such as JavaScript, image, and style files.

要保證客戶端只能下載那些允許他們訪問的檔案,你應該把所有面向客戶端的資原始檔都放在 /dist 目錄下,並且只允許客戶端請求來自 /dist 目錄下的檔案。

To ensure that clients can only download the files that they are permitted to see, put all client-facing asset files in the /dist folder and only honor requests for files from the /dist folder.

下列 Node.js Express 程式碼會把剩下的所有請求都路由到 /dist 目錄下,如果檔案未找到,就會返回 404 - NOT FOUND

The following Node.js Express code routes all remaining requests to /dist, and returns a 404 - NOT FOUND error if the file isn't found.

// Serve static files from /browser server.get('*.*', express.static(distFolder, { maxAge: '1y' }));
server.ts (static files)
      
      // Serve static files from /browser
server.get('*.*', express.static(distFolder, {
  maxAge: '1y'
}));
    

在伺服器端使用絕對 URL 進行 HTTP(資料)請求

Using absolute URLs for HTTP (data) requests on the server

本課程的 HeroServiceHeroSearchService 都委託 Angular 的 HttpClient 模組來獲取應用資料。這些服務會向 api/heroes 之類別的相對 URL 傳送請求。在伺服器端渲染的應用中,HTTP URL 必須是絕對的(例如,https://my-server.com/api/heroes )。這意味著當在伺服器上執行時,URL 必須以某種方式轉換為絕對 URL,而在瀏覽器中執行時,它們是相對 URL。

The tutorial's HeroService and HeroSearchService delegate to the Angular HttpClient module to fetch application data. These services send requests to relative URLs such as api/heroes. In a server-side rendered app, HTTP URLs must be absolute (for example, https://my-server.com/api/heroes). This means that the URLs must be somehow converted to absolute when running on the server and be left relative when running in the browser.

如果你正在使用 @nguniversal/*-engine 包之一(例如 @nguniversal/express-engine),就會自動為幫你做這件事。你無需再做任何事情來讓相對 URL 能在伺服器上執行。

If you are using one of the @nguniversal/*-engine packages (such as @nguniversal/express-engine), this is taken care for you automatically. You don't need to do anything to make relative URLs work on the server.

如果出於某種原因,你沒有使用 @nguniversal/*-engine 套件,你可能需要親自處理它。

If, for some reason, you are not using an @nguniversal/*-engine package, you may need to handle it yourself.

建議的解決方案是將完整的請求 URL 傳給 renderModule()renderModuleFactory()options 引數(具體取決於你在伺服器上渲染 AppServerModule 的目的)。此選項的侵入性最小,因為它不需要對應用進行任何更改。這裡的“請求 URL” 是指當應用在伺服器上渲染時的地址。例如,如果客戶端請求了 https://my-server.com/dashboard 並且要在伺服器上渲染該應用以響應該請求,那麼 options.url 應設定為 https://my-server.com/dashboard

The recommended solution is to pass the full request URL to the options argument of renderModule() or renderModuleFactory() (depending on what you use to render AppServerModule on the server). This option is the least intrusive as it does not require any changes to the app. Here, "request URL" refers to the URL of the request as a response to which the app is being rendered on the server. For example, if the client requested https://my-server.com/dashboard and you are rendering the app on the server to respond to that request, options.url should be set to https://my-server.com/dashboard.

現在,作為在伺服器端渲染應用的一部分,每次傳送 HTTP 請求時,Angular 都可以使用這裡提供的 options.url 正確地將請求 URL 解析為絕對 URL。

Now, on every HTTP request made as part of rendering the app on the server, Angular can correctly resolve the request URL to an absolute URL, using the provided options.url.