使用 RxJS timeout 操作符給 Angular SSR 伺服器端渲染模式下的 HTTP 請求新增超時機制

語言: CN / TW / HK

Angular Universal 是一個開源專案,擴充套件了 @angular/platform-server 的功能。 該專案使 Angular 中的伺服器端渲染成為可能。

為了在伺服器上渲染,Angular 使用 node.js 的 DOM 實現——domino. 對於每個 GET 請求,domino 都會建立一個類似的 Browser Document 物件。 在該物件上下文中,Angular 初始化應用程式,然後向後端發出請求,執行各種非同步任務,並將任何來自元件的更改檢測應用到 DOM,同時仍在 node.js 環境中執行。渲染引擎隨後將 DOM 序列化為字串並將字串提供給伺服器。伺服器將此 HTML 作為對 GET 請求的響應傳送。 伺服器上的 Angular 應用程式在渲染後被銷燬。

使用 Angular Universal 進行伺服器端渲染,最常見的一個問題就是,使用者在網站上開啟一個頁面並看到一個白屏。翻譯成 Web 應用領域的術語來說,就是首位元組時間(Time to First Byte, 簡稱 TTFB) 過大。TTFB 是指從瀏覽器請求頁面,到它從伺服器接收到第一個資訊位元組之間的時間。在這種情況下,瀏覽器確實想從伺服器接收響應,但請求以超時結束。

在分析這個效能問題之前,我們有必要了解 Zone.js 和 ApplicationRef.

Zone.js 是一個允許開發人員跟蹤非同步操作的工具。在它的幫助下,Angular 建立了自己的 Zone 並在其中啟動應用程式。在 Angular 區域中的每個非同步操作結束時,都會觸發更改檢測。

ApplicationRef 是對正在執行的應用程式(文件)的引用。在這個類的所有功能中,我們對 ApplicationRef.isStable 屬性感興趣。它是一個發出布林值的 Observable。當 Angular 區域中沒有非同步任務正在執行時,isStable 為 true,當有任何非同步任務時,isStable 為 false.

因此,應用程式的穩定性,取決於 Angular 區域中非同步任務的存在與否。

當應用程式的 ApplicationRef.isStable 第一次變為 true 時, Angular 會渲染當前狀態的應用程式,並清理平臺資源。

我們現在可以假設使用者正在嘗試開啟一個無法達到穩定狀態的應用程式。 setInterval、rxjs.interval 或在 Angular 區域中執行的任何其他遞迴的非同步操作,以及 HTTP 請求,都會阻止 Angular 應用進入穩定狀態。

我們可以使用 rxjs 的 timeout 操作符,強制使得一個長時間執行的 HTTP 請求超時。

```typescript import { timeout, catchError } from 'rxjs/operators'; import { of } from 'rxjs/observable/of';

http.get('https://example.com') .pipe( timeout(2000), catchError(e => of(null)) ).subscribe()

``` 上面例子的邏輯是,如果 HTTP 請求兩秒內,沒有從伺服器端接收到響應,則進入 catchError 錯誤處理模組內部。

這個解決方案的缺點是,對於每個 HTTP 請求,我們都需要手動為其新增 timeout 操作符。

一種更加優雅的實現是,使用開發包 @ngx-ssr/timeout 裡的 NgxSsrTimeoutModule.

如果模組被匯入 AppServerModule,那麼 HTTP 請求超時將只對伺服器端渲染環境起作用。

```typescript import { NgModule } from '@angular/core'; import { ServerModule, } from '@angular/platform-server'; import { AppModule } from './app.module'; import { AppComponent } from './app.component'; import { NgxSsrTimeoutModule } from '@ngx-ssr/timeout';

@NgModule({ imports: [ AppModule, ServerModule, NgxSsrTimeoutModule.forRoot({ timeout: 500 }), ], bootstrap: [AppComponent], }) export class AppServerModule {} ```