本指南講的是Angular Universal(統(tǒng)一平臺),一項在服務(wù)端運(yùn)行 Angular 應(yīng)用的技術(shù)。
標(biāo)準(zhǔn)的 Angular 應(yīng)用會運(yùn)行在瀏覽器中,它會在 DOM 中渲染頁面,以響應(yīng)用戶的操作。而Angular Universal 會在服務(wù)端運(yùn)行,生成一些靜態(tài)的應(yīng)用頁面,稍后再通過客戶端進(jìn)行啟動。這意味著該應(yīng)用的渲染通常會更快,讓用戶可以在應(yīng)用變得完全可交互之前,先查看應(yīng)用的布局。
要了解 SSR 的其它技術(shù)和概念的詳細(xì)信息,請參閱這篇文章。
可以使用 ?Angular CLI
? 來輕松為應(yīng)用做好服務(wù)端渲染的準(zhǔn)備。CLI 的 ?@nguniversal/express-engine
? 模板會執(zhí)行如下必要步驟。
Angular Universal 需要活躍 LTS 或 維護(hù)中 LTS版本的 Node.js。參見 package.json 文件中的 ?
engines
?屬性,以了解當(dāng)前支持的版本。
注意:
下載已完成的范例代碼,并將其運(yùn)行在一個 Node.js? Express 服務(wù)器中。
這次演練的基礎(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
?,請運(yùn)行以下 CLI 命令。
ng add @nguniversal/express-engine
該命令會創(chuàng)建如下文件夾結(jié)構(gòu)。
標(biāo)有 ?*
? 的文件都是新增的,不在原始的教程范例中。
要使用 Universal 在本地系統(tǒng)中渲染你的應(yīng)用,請使用如下命令。
npm run dev:ssr
打開瀏覽器,導(dǎo)航到 ?http://localhost:4200
?。你會看到熟悉的“英雄之旅”儀表盤頁面。
通過 ?routerLinks
?導(dǎo)航時能正常工作,因為它們使用的是內(nèi)置的鏈接元素(?<a>
?)。你可以從儀表盤進(jìn)入 英雄列表頁面,然后返回。你可以點擊儀表盤頁面上的一個英雄來顯示他的詳情頁面。
如果你限制下網(wǎng)速(稍后會講操作步驟),讓客戶端腳本下載時間變長,你會注意到:
不支持除了點擊 routerLink
以外的任何用戶事件。你必須等待完整的客戶端應(yīng)用啟動并運(yùn)行,或者使用 preboot 之類的庫來緩沖這些事件,這樣你就可以在客戶端腳本加載完畢后重放這些事件。
在開發(fā)機(jī)器上,從服務(wù)端渲染的應(yīng)用過渡到客戶端應(yīng)用的過程會很快,但是你還是應(yīng)該在實際場景中測試一下你的應(yīng)用。
你可以通過模擬速度較慢的網(wǎng)絡(luò)來更清晰地看到這種轉(zhuǎn)換,如下所示:
服務(wù)端渲染的應(yīng)用仍然可以快速啟動,但完整的客戶端應(yīng)用可能需要幾秒鐘才能加載完。
有三個主要的理由來為你的應(yīng)用創(chuàng)建一個 Universal 版本。
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è)備不支持 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)的,而當(dāng)完整的應(yīng)用加載完之后,又可以獲得完整的交互體驗。
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ù)器的代理進(jìn)行響應(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ù)器。
每次請求都會給出所請求路由的一個適當(dāng)?shù)囊晥D。?renderModule()
? 在模板中的 ?<app>
? 標(biāo)記中渲染出這個視圖,并為客戶端創(chuàng)建一個完成的 HTML 頁面。
最后,服務(wù)器就會把渲染好的頁面返回給客戶端。
由于 Universal 應(yīng)用并沒有運(yùn)行在瀏覽器中,因此該服務(wù)器上可能會缺少瀏覽器的某些 API 和其它能力。
比如,服務(wù)端應(yīng)用不能引用瀏覽器獨有的全局對象,比如 ?window
?、?document
?、?navigator
?或 ?location
?。
Angular 提供了一些這些對象的可注入的抽象層,比如 ?Location
?或 ?DOCUMENT
?,它可以作為你所調(diào)用的 API 的等效替身。如果 Angular 沒有提供它,你也可以寫一個自己的抽象層,當(dāng)在瀏覽器中運(yùn)行時,就把它委托給瀏覽器 API,當(dāng)它在服務(wù)器中運(yùn)行時,就提供一個符合要求的代用實現(xiàn)(也叫墊片 - shimming)。
同樣,由于沒有鼠標(biāo)或鍵盤事件,因此 Universal 應(yīng)用也不能依賴于用戶點擊某個按鈕來顯示某個組件。Universal 應(yīng)用必須僅僅根據(jù)客戶端過來的請求決定要渲染的內(nèi)容。把該應(yīng)用做成可路由的,就是一種好方案。
?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)用程序的根 |
extraProviders
|
這是可選的,可以讓你指定僅在服務(wù)器渲染應(yīng)用程序時才適用的依賴提供者。當(dāng)你的應(yīng)用需要某些只能由當(dāng)前運(yùn)行的服務(wù)器實例確定的信息時,可以執(zhí)行此操作。 |
?ngExpressEngine()
? 函數(shù)返回了一個會解析成渲染好的頁面的承諾(Promise)。接下來你的引擎要決定拿這個頁面做點什么。在這個引擎的 ?Promise
?回調(diào)函數(shù)中,把渲染好的頁面返回給了 Web 服務(wù)器,然后服務(wù)器通過 HTTP 響應(yīng)把它轉(zhuǎn)發(fā)給了客戶端。
注意:
這個包裝器幫助隱藏了 ?renderModule()
? 的復(fù)雜性。 在 Universal 代碼庫中還有更多針對其它后端技術(shù)的包裝器。
注意:
當(dāng)使用 NgUniversal Express 原理圖時,將自動處理稍后描述的基本行為。當(dāng)你要嘗試?yán)斫馄涞讓有袨榛蛟诓皇褂迷韴D的情況下自行實現(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ùn)的是,應(yīng)用的路由具有一些共同特征:它們的 URL 一般不帶文件擴(kuò)展名。(數(shù)據(jù)請求也可能缺少擴(kuò)展名,但是它們很容易識別出來,因為它們總是以 ?/api
? 開頭,所有的靜態(tài)資源的請求都會帶有一個擴(kuò)展名,比如 ?main.js
? 或 ?/node_modules/zone.js/dist/zone.js
?)。
由于使用了路由,所以我們可以輕松的識別出這三種類型的請求,并分別處理它們。
路由請求類型 |
詳情 |
---|---|
數(shù)據(jù)請求 |
請求的 URL 用 |
應(yīng)用導(dǎo)航 |
請求的 URL 不帶擴(kuò)展名。 |
靜態(tài)資產(chǎn) |
所有其它請求。 |
Node.js Express 服務(wù)器是一系列中間件構(gòu)成的管道,它會挨個對 URL 請求進(jìn)行過濾和處理。你可以調(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)用,并且模擬了遠(yuǎn)端數(shù)據(jù)服務(wù)器的行為。在實踐中,你應(yīng)該移除這個模塊,并且在服務(wù)器上注冊你的 Web API 中間件。
下列代碼會過濾出不帶擴(kuò)展名的 URL,并把它們當(dāng)做導(dǎo)航請求進(jìn)行處理。
// All regular routes use the Universal engine
server.get('*', (req, res) => {
res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
});
單獨的 ?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'
}));
本教程的 ?HeroService
?和 ?HeroSearchService
?都委托 Angular 的 ?HttpClient
?模塊來獲取應(yīng)用數(shù)據(jù)。這些服務(wù)會向 ?api/heroes
? 之類的相對 URL 發(fā)送請求。在服務(wù)端渲染的應(yīng)用中,HTTP URL 必須是絕對的(比如,?https://my-server.com/api/heroes
?)。這意味著當(dāng)在服務(wù)器上運(yùn)行時,URL 必須以某種方式轉(zhuǎn)換為絕對 URL,而在瀏覽器中運(yùn)行時,它們是相對 URL。
如果你正在使用 ?@nguniversal/*-engine
? 包之一(比如 ?@nguniversal/express-engine
?),就會自動為幫你做這件事。你無需再做任何事情來讓相對 URL 能在服務(wù)器上運(yùn)行。
如果出于某種原因,你沒有使用 ?@nguniversal/*-engine
? 包,你可能需要親自處理它。
建議的解決方案是將完整的請求 URL 傳給 ?renderModule()
? 或 ?renderModuleFactory()
? 的 ?options
?參數(shù)(具體取決于你在服務(wù)器上渲染 ?AppServerModule
?的目的)。此選項的侵入性最小,因為它不需要對應(yīng)用進(jìn)行任何更改。這里的“請求 URL” 是指當(dāng)應(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)視模式運(yùn)行并在每次更改后刷新瀏覽器。這個命令要比實際的 ?ng serve ? 命令慢。 |
? ng build && ng run app-name:server ? |
此命令會在生產(chǎn)模式下構(gòu)建服務(wù)器腳本和應(yīng)用程序。當(dāng)你要構(gòu)建用于部署的項目時,請使用此命令。 |
? npm run serve:ssr ? |
注意:此命令啟動服務(wù)器腳本,用于通過服務(wù)器端渲染在本地為應(yīng)用程序提供服務(wù)。它使用由 ? ng run build:ssr ? 創(chuàng)建的構(gòu)建工件,因此請確保你也運(yùn)行了該命令。 |
? npm run prerender ? |
此腳本可用于預(yù)先渲染應(yīng)用程序的頁面。 |
更多建議: