将 Angular HttpClient 迁移到使用 withFetch() 的 Fetch 后端
了解为何 Angular 的 HttpClient 正从 XMLHttpRequest 转向 fetch,探究如何通过 fetch() 实现边缘服务器端渲染与流式传输,以及这对拦截器意味着什么。
来源: Stop Using XMLHttpRequest: Why Angular 22 Shifted to the Native Fetch API
在 Angular 的发展历程中,其 HttpClient 长期以来都是建立在 XMLHttpRequest 之上的 RxJS 接口层,而 XMLHttpRequest 是现代网页平台中出现得最早的一种浏览器 API。然而,当应用程序需要在服务器端渲染、在边缘运行时环境中执行,或处理流式响应时,这种设计逐渐变成了弊端。本指南将阐述为何基于 fetch 的后端方案如此重要,如何通过一个简单的配置选项实现切换,以及现有的拦截器会有哪些变化、哪些保持不变。
为何 XMLHttpRequest 成为负担
XHR起源于1999年,当时微软为支持Outlook Web Access而创造了它。其设计暴露出它的陈旧性:它是状态化的、事件驱动的,且代码结构较为复杂,近十年来更广泛的JavaScript生态系统已逐渐远离它。大多数库和运行时早就选择了基于Promise的fetch()接口。Angular之所以仍在底层使用XHR,主要是因为它极其重视向后兼容性。
当Angular开始采用无区域变化检测和服务器端渲染技术后,这种权衡关系发生了变化。在那种架构下,XHR不再仅仅是没人愿意触碰的旧代码,它还限制了Angular应用能够运行的环境和方式。基于fetch的后端让该框架能够升级其网络架构,更好地处理流式数据,并在边缘平台上运行。
在撰写本文时,最新的 Angular 版本倾向于将 fetch 作为 HttpClient 的首选引擎。不同主要版本之间的默认设置和已废弃功能会有所变化,因此请查看您所用版本的发布说明,而非自行假设当前使用的是哪种后端机制。
Edge 运行时不提供 XMLHttpRequest
后端托管方式正在从 Docker 容器中的完整 Node.js 进程,转向 Cloudflare Workers、Deno 和 Fastly Compute 等轻量级的 V8 孤立环境。如果您想更深入地了解这种运行时模型,可参阅我们关于边缘孤立环境、WebAssembly 与 Node.js 运行时的对比的文章。
这些平台都是基于现代网页标准 API 构建的,而 XMLHttpRequest 并不在其中。当传统 Angular SSR 构建在 such 平台上时,一旦 HttpClient 在页面渲染过程中发起首次 API 调用,就会因为缺乏相应的传输机制而失败。
相比之下,fetch() 在浏览器、Node.js、Deno 以及各类边缘运行时中都可用。一旦 HttpClient 开始使用它,同一个 SSR 应用便具备了可移植性:你可以在 Cloudflare Workers 或 Deno Deploy 中渲染它,并从地理位置上更接近用户的地点发起 API 调用。
使用 withFetch() 更换后端
这属于配置变更,而非重构。组件和服务仍会像以前一样调用 this.http.get() 及其他相关方法;只有应用程序配置中的提供者设置发生了变化。
下面的代码片段通过 provideHttpClient() 注册 HttpClient,并传入两个功能选项。withFetch() 用于选择基于 Fetch 的传输方式,而 withInterceptors() 则用于注册稍后定义的函数式身份验证拦截器。
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './core/auth.interceptor';
export const appConfig: ApplicationConfig = {
providers: [
// Injects HttpClient using the modern Fetch API backend
provideHttpClient(
withFetch(),
withInterceptors([authInterceptor])
)
]
};
在内部,withFetch() 会指示 Angular 使用 FetchBackend 而非 HttpXhrBackend 作为实际执行网络请求的组件。该层之上的所有内容保持不变。
在启用该功能之前需注意的一个限制是:fetch 后端不会报告上传进度事件,因为 fetch() 并没有提供类似 XHR 的上传进度功能。如果您的应用需要显示文件上传的进度条,务必对相关流程进行专门测试。
无需缓冲即可流式获取响应
XHR 是以完整且独立的响应为基础设计的,它在处理大型数据或连续数据时表现不佳:下载 50MB 的文件或接收分块传输的响应时,JavaScript 开始处理之前内存中往往会积累大量缓冲数据,从而导致内存压力增大及界面响应迟缓。
fetch() 的设计以 ReadableStream 为核心,因此数据从网络传来时可以逐块被读取。借助 fetch 后端,Angular 可以在数据到达的瞬间进行处理,而无需等待全部数据加载完成。
这对于类似 AI 的界面尤为重要。能够读取 Server-Sent Events 或分块生成的 LLM 输出的聊天界面,需要在内容生成的同时立即显示对应文本,而具备流式传输功能的传输机制正是实现这一目标的基础,同时避免了所有数据堆积在浏览器内存中。
不过,要明确你所获取的内容。普通的 http.get() 可观察对象在响应体传输完成后仍会发出最终响应;而分步获取数据则通过进度事件实现(使用 reportProgress 并设置 observe: 'events'),对于长时间运行的令牌流,许多团队仍会选择 EventSource 或直接调用 fetch()。请根据你所使用的 Angular 版本的文档来确认所需的行为。
拦截器会如何变化
每当网络引擎发生变更时,经验丰富的工程师首先会询问的是中间件相关问题:身份验证和日志记录拦截器还能正常工作吗?
有了功能型拦截器,答案是肯定的。拦截操作基于 Angular 自带的 HttpRequest 和 HttpResponse 抽象类,它们位于传输层之上。拦截器会修改 Angular 的请求对象,只有当整个处理链执行完毕后,最终的请求才会到达 FetchBackend,由其转换为 fetch() 调用。拦截器从不直接操作原生请求。
下面的示例与后端实现无关。它读取 JWT,通过添加 Authorization 标头复制不可变的请求对象,然后将副本传递给 next,后者再将其转发给下一个拦截器,最终送达后端。
import { HttpInterceptorFn } from '@angular/common/http';
// This interceptor works perfectly with both XHR and Fetch backends
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = localStorage.getItem('jwt');
// Clone and mutate the Angular abstraction, not the native request
const secureReq = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
// Hand off to the next interceptor, and ultimately, the Fetch backend
return next(secureReq);
};
这一特定代码片段存在两个需要注意的问题,而非 fetch 后端本身。首先,在服务器端渲染时并不存在 localStorage,因此在 SSR 或边缘计算部署环境中应采用其他方式来保护该调用或注入令牌。其次,将令牌存储在 localStorage 中会使其暴露于任何 XSS 漏洞之下,而且当没有令牌时代码会发送 Bearer null;生产环境中的代码应能够处理这两种情况。
基于类的拦截器仍然有效
那些以实现 HttpInterceptor 的类形式编写的旧拦截器仍可在 fetch 后端环境中正常使用。不过,它们与框架的其他部分越来越不协调。将 withFetch() 与通过 withInterceptors() 注册的函数式拦截器结合使用,才是符合当前 Angular 规范的做法,因此在修改这类基于类的拦截器时,值得进行迁移。
关键要点
- 将
HttpClient从 XHR 更改为 fetch 是一项看似细微却意义重大的升级:仅需一个提供者选项,调用代码无需任何改动。 - 如果当前版本默认未使用 fetch,请在
provideHttpClient()中添加withFetch()。 - 正是 fetch 使得 Angular SSR 能在 Cloudflare Workers、Deno 以及那些缺乏
XMLHttpRequest的类似边缘运行时环境中运行。
ReadableStream构建,这有助于处理大型数据负载以及分块响应(如大语言模型输出),但在基于其构建流式用户界面之前,需先了解HttpClient如何呈现部分数据。HttpInterceptorFn)仍可正常使用;切换后请测试上传进度以及所有仅适用于服务端渲染的代码路径。