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

tRPC Vanilla 客户端中的过程调用取消:使用 AbortController 与 AbortSignal 中止 query / mutation 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 APIAbortController用于创建控制信号、并能触发中止的控制器对象AbortSignal由AbortController提供用于向请求传递“应被中止”的通知。在 tRPC 的 Vanilla 客户端中只需要做两件事即可获得可取消的过程调用在调用.query()或.mutate()时把AbortSignal放进第二个参数procedure options中需要取消时调用对应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 createTRPCClientAppRouter({ 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 链路传给各个 linksignal标准中止信号用于取消本次过程调用。在 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: MaybeAbortSignal) { 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被透传给底层fetch的RequestInit从而在浏览器、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 createTRPCClientAppRouter({ 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 rejectiontry { 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这也是客户端无论是否带取消逻辑都不适用的场景。另外需要注意httpLink与httpBatchLink只支持 query 与 mutation遇到subscription类型会直接抛出“请使用httpSubscriptionLink或wsLink”的错误参见 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.ts将op.signal交给请求器请求发出httpUtils.ts先throwIfAborted再把 signal 传给fetch批处理链路httpBatchLink.ts用allAbortSignals合并批内信号信号工具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),仅供参考