Angular 服務(wù)端渲染

2022-07-13 09:36 更新

Angular Universal:Angular 統(tǒng)一平臺簡介

本指南講的是Angular Universal(統(tǒng)一平臺),一項在服務(wù)端運行 Angular 應(yīng)用的技術(shù)。

標準的 Angular 應(yīng)用會運行在瀏覽器中,它會在 DOM 中渲染頁面,以響應(yīng)用戶的操作。而Angular Universal 會在服務(wù)端運行,生成一些靜態(tài)的應(yīng)用頁面,稍后再通過客戶端進行啟動。這意味著該應(yīng)用的渲染通常會更快,讓用戶可以在應(yīng)用變得完全可交互之前,先查看應(yīng)用的布局。

要了解 SSR 的其它技術(shù)和概念的詳細信息,請參閱這篇文章

可以使用 ?Angular CLI? 來輕松為應(yīng)用做好服務(wù)端渲染的準備。CLI 的 ?@nguniversal/express-engine? 模板會執(zhí)行如下必要步驟。

Angular Universal 需要活躍 LTS 或 維護中 LTS版本的 Node.js。參見 package.json 文件中的 ?engines ?屬性,以了解當前支持的版本。

注意:
下載已完成的范例代碼,并將其運行在一個 Node.js? Express 服務(wù)器中。

Universal 教程

這次演練的基礎(chǔ)是“英雄之旅”教程

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

要創(chuàng)建服務(wù)端應(yīng)用模塊 ?app.server.module.ts?,請運行以下 CLI 命令。

ng add @nguniversal/express-engine

該命令會創(chuàng)建如下文件夾結(jié)構(gòu)。


標有 ?*? 的文件都是新增的,不在原始的教程范例中。

Universal 實戰(zhàn)

要使用 Universal 在本地系統(tǒng)中渲染你的應(yīng)用,請使用如下命令。

npm run dev:ssr

打開瀏覽器,導(dǎo)航到 ?http://localhost:4200?。你會看到熟悉的“英雄之旅”儀表盤頁面。

通過 ?routerLinks ?導(dǎo)航時能正常工作,因為它們使用的是內(nèi)置的鏈接元素(?<a>?)。你可以從儀表盤進入 英雄列表頁面,然后返回。你可以點擊儀表盤頁面上的一個英雄來顯示他的詳情頁面。

如果你限制下網(wǎng)速(稍后會講操作步驟),讓客戶端腳本下載時間變長,你會注意到:

  • 你無法添加或刪除英雄
  • 儀表盤頁面上的搜索框會被忽略
  • “詳情”頁面上的后退保存按鈕不起作用

不支持除了點擊 routerLink 以外的任何用戶事件。你必須等待完整的客戶端應(yīng)用啟動并運行,或者使用 preboot 之類的庫來緩沖這些事件,這樣你就可以在客戶端腳本加載完畢后重放這些事件。

在開發(fā)機器上,從服務(wù)端渲染的應(yīng)用過渡到客戶端應(yīng)用的過程會很快,但是你還是應(yīng)該在實際場景中測試一下你的應(yīng)用。

你可以通過模擬速度較慢的網(wǎng)絡(luò)來更清晰地看到這種轉(zhuǎn)換,如下所示:

  1. 打開 Chrome 開發(fā)者工具,進入 Network 標簽頁。
  2. 找一下菜單欄最右側(cè)的 Network Throttling 下拉菜單。
  3. 嘗試一下 “3G” 的速度吧。

服務(wù)端渲染的應(yīng)用仍然可以快速啟動,但完整的客戶端應(yīng)用可能需要幾秒鐘才能加載完。

為何需要服務(wù)端渲染?

有三個主要的理由來為你的應(yīng)用創(chuàng)建一個 Universal 版本。

幫助網(wǎng)絡(luò)爬蟲(SEO)

Google、Bing、Facebook、Twitter 和其它社交媒體網(wǎng)站都依賴網(wǎng)絡(luò)爬蟲去索引你的應(yīng)用內(nèi)容,并且讓它的內(nèi)容可以通過網(wǎng)絡(luò)搜索到。 這些網(wǎng)絡(luò)爬蟲可能不會像人類那樣導(dǎo)航到你的具有高度交互性的 Angular 應(yīng)用,并為其建立索引。

Angular Universal 可以為你生成應(yīng)用的靜態(tài)版本,它易搜索、可鏈接,瀏覽時也不必借助 JavaScript。它也讓站點可以被預(yù)覽,因為每個 URL 返回的都是一個完全渲染好的頁面。

提升手機和低功耗設(shè)備上的性能

有些設(shè)備不支持 JavaScript 或 JavaScript 執(zhí)行得很差,導(dǎo)致用戶體驗不可接受。對于這些情況,你可能會需要該應(yīng)用的服務(wù)端渲染的、無 JavaScript 的版本。雖然有一些限制,不過這個版本可能是那些完全沒辦法使用該應(yīng)用的人的唯一選擇。

快速顯示第一頁

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

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

在實踐中,你可能要使用一個著陸頁的靜態(tài)版本來保持用戶的注意力。同時,你也會在幕后加載完整的 Angular 應(yīng)用。用戶會覺得著陸頁幾乎是立即出現(xiàn)的,而當完整的應(yīng)用加載完之后,又可以獲得完整的交互體驗。

Universal Web 服務(wù)器

Universal Web 服務(wù)器使用 Universal 模板引擎渲染出的靜態(tài) HTML 來響應(yīng)對應(yīng)用頁面的請求。 服務(wù)器接收并響應(yīng)來自客戶端(通常是瀏覽器)的 HTTP 請求,并回復(fù)靜態(tài)文件,如腳本、CSS 和圖片。 它可以直接響應(yīng)數(shù)據(jù)請求,也可以作為獨立數(shù)據(jù)服務(wù)器的代理進行響應(yīng)。

這個例子中的范例 Web 服務(wù)器是基于常見的 Express 框架的。

注意:
任何一種 Web 服務(wù)器技術(shù)都可以作為 Universal 應(yīng)用的服務(wù)器,只要它能調(diào)用 Universal 的 ?renderModule()? 函數(shù)。 這里所討論的這些原則和決策點也適用于任何 Web 服務(wù)器技術(shù)。

Universal 應(yīng)用使用 ?platform-server? 包(而不是 ?platform-browser?),它提供了 DOM 的服務(wù)端實現(xiàn)、?XMLHttpRequest ?以及其它不依賴瀏覽器的底層特性。

服務(wù)器(這個例子中使用的是 Node.js Express 服務(wù)器)會把客戶端對應(yīng)用頁面的請求傳給 NgUniversal 的 ?ngExpressEngine?。在內(nèi)部實現(xiàn)上,它會調(diào)用 Universal 的 ?renderModule()? 函數(shù),它還提供了緩存等有用的工具函數(shù)。

?renderModule()? 函數(shù)接受一個模板 HTML 頁面(通常是 ?index.html?)、一個包含組件的 Angular 模塊和一個用于決定該顯示哪些組件的路由作為輸入。 該路由從客戶端的請求中傳給服務(wù)器。

每次請求都會給出所請求路由的一個適當?shù)囊晥D。?renderModule()? 在模板中的 ?<app>? 標記中渲染出這個視圖,并為客戶端創(chuàng)建一個完成的 HTML 頁面。

最后,服務(wù)器就會把渲染好的頁面返回給客戶端。

使用瀏覽器 API

由于 Universal 應(yīng)用并沒有運行在瀏覽器中,因此該服務(wù)器上可能會缺少瀏覽器的某些 API 和其它能力。

比如,服務(wù)端應(yīng)用不能引用瀏覽器獨有的全局對象,比如 ?window?、?document?、?navigator ?或 ?location?。

Angular 提供了一些這些對象的可注入的抽象層,比如 ?Location ?或 ?DOCUMENT?,它可以作為你所調(diào)用的 API 的等效替身。如果 Angular 沒有提供它,你也可以寫一個自己的抽象層,當在瀏覽器中運行時,就把它委托給瀏覽器 API,當它在服務(wù)器中運行時,就提供一個符合要求的代用實現(xiàn)(也叫墊片 - shimming)。

同樣,由于沒有鼠標或鍵盤事件,因此 Universal 應(yīng)用也不能依賴于用戶點擊某個按鈕來顯示某個組件。Universal 應(yīng)用必須僅僅根據(jù)客戶端過來的請求決定要渲染的內(nèi)容。把該應(yīng)用做成可路由的,就是一種好方案。

Universal 模板引擎

?server.ts? 文件中最重要的部分是 ?ngExpressEngine()? 函數(shù)。

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

?ngExpressEngine()? 是對 Universal 的 ?renderModule()? 函數(shù)的封裝。它會把客戶端請求轉(zhuǎn)換成服務(wù)端渲染的 HTML 頁面。它接受一個具有下列屬性的對象:

屬性

詳情

bootstrap

在服務(wù)器上渲染時用于引導(dǎo)應(yīng)用程序的根 NgModule 或 NgModule 工廠。對于這個范例應(yīng)用,它是 AppServerModule。它是 Universal 服務(wù)端渲染器和 Angular 應(yīng)用之間的橋梁。

extraProviders

這是可選的,可以讓你指定僅在服務(wù)器渲染應(yīng)用程序時才適用的依賴提供者。當你的應(yīng)用需要某些只能由當前運行的服務(wù)器實例確定的信息時,可以執(zhí)行此操作。

?ngExpressEngine()? 函數(shù)返回了一個會解析成渲染好的頁面的承諾(Promise)。接下來你的引擎要決定拿這個頁面做點什么。在這個引擎的 ?Promise ?回調(diào)函數(shù)中,把渲染好的頁面返回給了 Web 服務(wù)器,然后服務(wù)器通過 HTTP 響應(yīng)把它轉(zhuǎn)發(fā)給了客戶端。

注意:
這個包裝器幫助隱藏了 ?renderModule()? 的復(fù)雜性。 在 Universal 代碼庫中還有更多針對其它后端技術(shù)的包裝器。

過濾請求的 URL

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

Web 服務(wù)器必須把對應(yīng)用頁面的請求和其它類型的請求區(qū)分開。

這可不像攔截對根路徑 ?/? 的請求那么簡單。瀏覽器可以請求應(yīng)用中的任何一個路由地址,比如 ?/dashboard?、?/heroes? 或 ?/detail:12?。事實上,如果應(yīng)用會通過服務(wù)器渲染,那么應(yīng)用中點擊的任何一個鏈接都會發(fā)到服務(wù)器,就像導(dǎo)航時的地址會發(fā)到路由器一樣。

幸運的是,應(yīng)用的路由具有一些共同特征:它們的 URL 一般不帶文件擴展名。(數(shù)據(jù)請求也可能缺少擴展名,但是它們很容易識別出來,因為它們總是以 ?/api? 開頭,所有的靜態(tài)資源的請求都會帶有一個擴展名,比如 ?main.js? 或 ?/node_modules/zone.js/dist/zone.js?)。

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

路由請求類型

詳情

數(shù)據(jù)請求

請求的 URL 用 /api 開頭。

應(yīng)用導(dǎo)航

請求的 URL 不帶擴展名。

靜態(tài)資產(chǎn)

所有其它請求。

Node.js Express 服務(wù)器是一系列中間件構(gòu)成的管道,它會挨個對 URL 請求進行過濾和處理。你可以調(diào)用 ?app.get()? 來配置 Express 服務(wù)器的管道,就像下面這個數(shù)據(jù)請求一樣。

// TODO: implement data requests securely
server.get('/api/**', (req, res) => {
  res.status(404).send('data requests are not yet supported');
});
注意:
這個范例服務(wù)器不會處理數(shù)據(jù)請求。
本教程的“內(nèi)存 Web API” 模塊(一個演示及開發(fā)工具)攔截了所有 HTTP 調(diào)用,并且模擬了遠端數(shù)據(jù)服務(wù)器的行為。在實踐中,你應(yīng)該移除這個模塊,并且在服務(wù)器上注冊你的 Web API 中間件。

下列代碼會過濾出不帶擴展名的 URL,并把它們當做導(dǎo)航請求進行處理。

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

安全的提供靜態(tài)文件

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

要保證客戶端只能下載那些允許他們訪問的文件,你應(yīng)該把所有面向客戶端的資源文件都放在 ?/dist? 目錄下,并且只允許客戶端請求來自 ?/dist? 目錄下的文件。

下列 Node.js Express 代碼會把剩下的所有請求都路由到 ?/dist? 目錄下,如果文件未找到,就會返回 ?404 - NOT FOUND?。

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

在服務(wù)端使用絕對 URL 進行 HTTP(數(shù)據(jù))請求

本教程的 ?HeroService ?和 ?HeroSearchService ?都委托 Angular 的 ?HttpClient ?模塊來獲取應(yīng)用數(shù)據(jù)。這些服務(wù)會向 ?api/heroes? 之類的相對 URL 發(fā)送請求。在服務(wù)端渲染的應(yīng)用中,HTTP URL 必須是絕對的(比如,?https://my-server.com/api/heroes?)。這意味著當在服務(wù)器上運行時,URL 必須以某種方式轉(zhuǎn)換為絕對 URL,而在瀏覽器中運行時,它們是相對 URL。

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

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

建議的解決方案是將完整的請求 URL 傳給 ?renderModule()? 或 ?renderModuleFactory()? 的 ?options ?參數(shù)(具體取決于你在服務(wù)器上渲染 ?AppServerModule ?的目的)。此選項的侵入性最小,因為它不需要對應(yīng)用進行任何更改。這里的“請求 URL” 是指當應(yīng)用在服務(wù)器上渲染時的地址。比如,如果客戶端請求了 ?https://my-server.com/dashboard? 并且要在服務(wù)器上渲染該應(yīng)用以響應(yīng)該請求,那么 ?options.url? 應(yīng)設(shè)置為 ?https://my-server.com/dashboard?。

現(xiàn)在,作為在服務(wù)端渲染應(yīng)用的一部分,每次發(fā)送 HTTP 請求時,Angular 都可以使用這里提供的 ?options.url? 正確地將請求 URL 解析為絕對 URL。

實用腳本

 腳本  詳情
 ?npm run dev:ssr?  此命令類似于 ?ng serve?,它在開發(fā)期間提供實時重新加載,但使用服務(wù)器端渲染。該應(yīng)用程序以監(jiān)視模式運行并在每次更改后刷新瀏覽器。這個命令要比實際的 ?ng serve? 命令慢。
? ng build && ng run app-name:server?  此命令會在生產(chǎn)模式下構(gòu)建服務(wù)器腳本和應(yīng)用程序。當你要構(gòu)建用于部署的項目時,請使用此命令。
npm run serve:ssr?
注意:
?serve:ssr? 不能用于在生產(chǎn)環(huán)境為你的應(yīng)用程序提供服務(wù),而僅用于在本地測試服務(wù)器端渲染的應(yīng)用程序。
此命令啟動服務(wù)器腳本,用于通過服務(wù)器端渲染在本地為應(yīng)用程序提供服務(wù)。它使用由 ?ng run build:ssr? 創(chuàng)建的構(gòu)建工件,因此請確保你也運行了該命令。
? npm run prerender?  此腳本可用于預(yù)先渲染應(yīng)用程序的頁面。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號