使用 RxJS timeout 操作符给 Angular SSR 服务器端渲染模式下的 HTTP 请求添加超时机制

发布于 2022年 05月 19日 13:05

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 请求超时。

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 请求超时将只对服务器端渲染环境起作用。

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 {}

推荐文章