tRPC Vanilla 客户端中的过程调用取消:使用 AbortController 与 AbortSignal 中止 query / mutation
2026/9/10 22:41:15 网站建设 项目流程

tRPC Vanilla 客户端中的过程调用取消:使用 AbortController 与 AbortSignal 中止 query / mutation

【免费下载链接】trpc🧙‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc

导读

本文围绕 aborting-procedures.md 展开,讲解如何基于 Web 平台标准的AbortController/AbortSignalAPI,在 tRPC Vanilla 客户端(即createTRPCClient建立的、与框架无关的调用端)中按需取消查询(query)与变更(mutation)请求。读完本文,你将掌握取消信号在 query/mutation 选项中的正确传递方式、信号经由 links 链路最终作用于底层fetch的底层原理,以及 HTTP 批处理(batching)场景下信号合并带来的行为差异,从而为组件卸载、超时控制、用户主动停止等常见需求写出可靠代码。

一、tRPC 对取消操作的支持方式

tRPC 客户端并没有自创一套取消机制,而是直接复用标准 Web API:

  • AbortController:用于创建控制信号、并能触发中止的控制器对象;
  • AbortSignal:由AbortController提供,用于向请求传递“应被中止”的通知。

在 tRPC 的 Vanilla 客户端中,只需要做两件事即可获得可取消的过程调用:

  1. 在调用.query().mutate()时,把AbortSignal放进第二个参数(procedure options)中;
  2. 需要取消时,调用对应AbortController实例的.abort()方法。

这也意味着本文介绍的 API 不依赖 React 等任何 UI 框架——在原生 TS 项目、独立后端服务,或尚无官方集成的框架中使用 Vanilla Client 时同样生效。

二、官方最小示例:三分步走

原文档给出了一个可以直接运行的完整示例,核心逻辑是三步:创建控制器、传递信号、按需中止。

// server.ts —— 定义一个用于演示的路由 import { initTRPC } from '@trpc/server'; import { z } from 'zod'; const t = initTRPC.create(); const appRouter = t.router({ userById: t.procedure .input(z.string()) .query(({ input }) => ({ id: input, name: 'Bilbo' })), }); export type AppRouter = typeof appRouter; // client.ts —— 建立 Vanilla 客户端并发起可取消调用 import { createTRPCClient, httpBatchLink } from '@trpc/client'; import type { AppRouter } from './server'; const client = createTRPCClient<AppRouter>({ links: [ httpBatchLink({ url: 'http://localhost:3000/trpc', }), ], }); // 1. 创建一个 AbortController 实例 —— 这是标准的 JavaScript API const ac = new AbortController(); // 2. 将 signal 传给 query 或 mutation 的选项 const query = client.userById.query('id_bilbo', { signal: ac.signal }); // 3. 需要取消时调用 abort() ac.abort();

要点拆解:

  • ac.signal通过过程调用的第二个参数传入,而不是作为过程入参(input)的一部分;
  • .query().mutate()的调用方式完全一致,都可携带{ signal }
  • 取消动作本身是一个同步调用,调用后底层请求会被中止,查询过程不再产生有效结果。

在 完整路由示例 中,你可以看到同样的client以 JavaScript Proxy 的形式暴露getUser.query()createUser.mutate()这类类型安全的调用入口,而中止能力就挂在这些类型安全的调用选项之上。

三、信号字段的类型定义:procedure options 的第二参数

为什么signal要放在第二个参数中?在源码层面,这个第二参数对应着客户端类型定义中的TRPCProcedureOptions

// 文件:packages/client/src/internals/types.ts#L96-L102 export interface TRPCProcedureOptions { /** * Client-side context */ context?: ClientContext; signal?: AbortSignal; }

从定义可见,该选项同时承载:

  • context:客户端侧上下文,会沿 links 链路传给各个 link;
  • signal:标准中止信号,用于取消本次过程调用。

在 TRPCUntypedClient 的实现 中,这个 options 会被逐层透传(例如其内部对 query/mutation 的封装中多处出现signal: opts?.signal的转发),最终在发起操作时把信号交给链接链路(link chain)中的终止链接处理。也就是说,signal从“用户传入”到“作用于请求”,是一个类型驱动的标准透传过程。

四、从 signal 到底层 fetch:终止链接中的传播链路

在 tRPC 中,真正发出 HTTP 请求的是 links 中的终止链接。不同的终止链接对signal的处理略有不同,但最终都汇聚到同一条底层路径——fetch

4.1 单请求场景:httpLink 直接透传

httpLink是典型的单请求终止链接。在其实现中,操作(operation)的signal被直接送入请求器:

// 文件:packages/client/src/links/httpLink.ts#L91-L108 const request = universalRequester({ ...resolvedOpts, type, path, input, signal: op.signal, // headers 处理…… });

这里的op.signal即来自调用时传入的{ signal: ac.signal }

4.2 发出请求前检查:throwIfAborted

在真正调用网络层之前,客户端会先做一次“已中止则立即抛出”的检查,见 fetchHTTPResponse 及其辅助函数:

const throwIfAborted = (signal: Maybe<AbortSignal>) => { if (!signal?.aborted) { return; } signal.throwIfAborted?.(); // 若运行环境没有原生 throwIfAborted,则回退到抛出 signal.reason 的实现 // …… }; export async function fetchHTTPResponse(opts: HTTPRequestOptions) { throwIfAborted(opts.signal); // …… 拼接 URL 与请求体 …… return getFetch(opts.fetch)(url, { method, signal: opts.signal, body, headers, }); }

这个实现的意义在于:

  • 如果请求尚未发出但信号已处于 aborted 状态throwIfAborted会立刻短路失败,避免一次无意义的网络往返;
  • 如果请求已在途signal被透传给底层fetchRequestInit,从而在浏览器、Deno、Bun 等遵循标准fetch语义的环境中自动获得“中止网络请求”的能力;
  • 代码先优先调用原生signal.throwIfAborted(),缺失时再自行抛出signal.reason,体现了对异构运行时(getFetch会按环境选取合适的 fetch 实现)的兼容处理。

五、批处理场景的信号合并:allAbortSignals 的特殊语义

当你使用httpBatchLink时,同一时间窗内的多个 query / mutation 会被合并为一个 HTTP 请求。此时单个操作的信号与“整批请求”之间并不是一一对应关系,而是经过一层合并处理。

dataLoader 批处理的 fetch 逻辑 中是这样构造整批请求信号的:

async fetch(batchOps) { const path = batchOps.map((op) => op.path).join(','); const inputs = batchOps.map((op) => op.input); const signal = allAbortSignals(...batchOps.map((op) => op.signal)); // …… 以合并后的 signal 发起整批 HTTP 请求 …… }

allAbortSignals定义在 packages/client/src/internals/signals.ts 中,其文档注释清楚地说明了合并语义:

类似于Promise.all()但作用于 abort signals:

  • 所有信号都已 abort 时,合并后的信号才会被 abort;
  • 如果某个信号为null,则该信号不会阻止整体 abort。

由此可以得出一个重要推论(也是源码结构确认的实现行为):

  • 当你 abort 某个批内操作时,并不会立即中止整批 HTTP 请求——整批请求会继续执行,直到批内所有操作都被 abort,或请求自然完成;
  • 这意味着在使用httpBatchLink的场景下,若你希望通过“取消单个过程”来释放网络资源,效果会受同一批次内其它操作的影响。如果某个操作需要严格的独立取消语义,可以考虑将其单独发出,或改用不合并请求的httpLink

六、常见使用场景与配套写法

6.1 超时取消:结合 setTimeout

标准信号 API 天然适合做“超时即取消”:

import { createTRPCClient, httpLink } from '@trpc/client'; import type { AppRouter } from './server'; const client = createTRPCClient<AppRouter>({ links: [httpLink({ url: 'http://localhost:3000/trpc' })], }); async function fetchUserWithTimeout(userId: string, timeoutMs = 5000) { const ac = new AbortController(); const timer = setTimeout(() => ac.abort(), timeoutMs); try { return await client.userById.query(userId, { signal: ac.signal }); } finally { clearTimeout(timer); } }

若用户需要自行结束任务(例如“停止下载”“停止搜索”按钮),只需把AbortController提升到组件或任务的生命周期中,由 UI 事件调用.abort()即可。

6.2 取消后的错误处理

信号被触发后,请求层会以中止原因(通常是运行时的AbortError之类的DOMException,或signal.reason)失败。因此,凡是可能被取消的调用都应具备相应的错误捕获路径,避免出现未处理的 promise rejection:

try { const user = await client.userById.query('id_bilbo', { signal: ac.signal }); // 正常处理结果…… } catch (cause) { // 区分“主动取消”与“真实错误” if (ac.signal.aborted) { console.log('请求已被用户取消', cause); } else { console.error('请求失败', cause); } }

在客户端内部,请求失败会统一经TRPCClientError.from(cause)包装后进入调用方的错误路径(可参见 httpLink 的错误处理)。因此实践中可以结合signal.aborted状态判断失败是否由取消引起。

6.3 订阅链路中的信号辅助函数

除了常规 HTTP 链路,packages/client/src/internals/signals.ts 还提供了另外两个信号工具,供内部(如订阅链路、去重链路)使用:

  • raceAbortSignals(...signals):等价于“信号版的Promise.race”,任一输入信号 abort 即触发合并信号 abort,实现类似AbortSignal.any的能力;
  • abortSignalToPromise(signal):把一个信号转换为一个永不 resolve、仅在 abort 时以signal.reason拒绝的 promise,便于把“取消”接入 promise 组合逻辑。

这些函数是面向库内部的实现细节,普通调用方通常无需直接使用;但理解它们有助于解释为什么批处理与订阅在取消行为上会有差异。

七、该在什么场景使用 Vanilla 客户端的取消能力

本文介绍的是Vanilla 客户端的过程取消。结合 overview.md 给出的选型建议,这一能力最适合以下情形:

  • 使用尚无官方集成的前端框架,直接用createTRPCClient调 API;
  • 在独立的 TypeScript 后端服务中作为服务间调用的调用方;
  • 需要在 React 之外(如 Node 脚本、CLI 工具)获得类型安全的端到端调用。

而如果你正在 React 组件内使用 TanStack React Query 集成,通常不需要手动创建AbortController——TanStack Query 自身已围绕AbortSignal构建了完善的取消与请求生命周期管理。此外,若你需要在同一个 API 进程内部调用自身过程,官方建议不要走网络层客户端,而应使用服务端直接调用方案(如createCaller),这也是客户端(无论是否带取消逻辑)都不适用的场景。

另外需要注意:httpLinkhttpBatchLink只支持 query 与 mutation,遇到subscription类型会直接抛出“请使用httpSubscriptionLinkwsLink”的错误(参见 httpLink.ts 与 httpBatchLink.ts)。订阅类操作的中止语义由 WebSocket / SSE 类链路另行负责。

八、小结

tRPC Vanilla 客户端的取消能力建立在人人熟悉的标准 Web API 之上,学习成本极低:创建AbortController→ 把ac.signal放进 query/mutation 的 options → 需要时调用ac.abort()。在仓库源码层面,这一能力的可靠性由以下环节共同保证:

环节关键位置职责
类型入口TRPCProcedureOptions声明 query/mutation 可接收signal
参数透传TRPCUntypedClient.ts将 options 中的信号逐层转发至 link
单请求链路httpLink.tsop.signal交给请求器
请求发出httpUtils.tsthrowIfAborted,再把 signal 传给fetch
批处理链路httpBatchLink.tsallAbortSignals合并批内信号
信号工具signals.tsallAbortSignals/raceAbortSignals/abortSignalToPromise

在动手之前,建议结合你的实际传输方式做一次取舍:使用httpLink(单请求)可获得最直接的“一取消即中止”体验;使用httpBatchLink(批处理)则要意识到单操作取消对整批请求的影响。在 examples 目录下的多个服务端/客户端示例中,你也能找到各种环境下建立 Vanilla 客户端的参考写法,将它们与本篇的信号传递方式结合,即可得到完整、可运行、可取消的类型安全调用。

【免费下载链接】trpc🧙‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询