tRPC 客户端 Links 链接链完全指南:数据流定制、自定义 Link 与终止 Link
tRPC 客户端 Links 链接链完全指南数据流定制、自定义 Link 与终止 Link【免费下载链接】trpc♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc本篇技术指南围绕 tRPC 官方文档中「Links Overview」一节www/versioned_docs/version-10.x/client/links/overview.md展开系统讲解 tRPC 客户端 Links 的核心设计什么是 link、如何将多个 link 组合成一条链接链、如何按三部分结构编写自定义 link、终止 link 的作用以及如何通过op.context在链接链内传递与修改上下文。读完本文你将掌握在 Next.js 集成或 vanilla 客户端中正确配置links数组、编写可复用的自定义 link、并按需选择httpBatchLink/httpLink/wsLink等终止 link 的完整实战能力同时理解其背后的类型系统与可观察对象observable机制。Links 是什么连接客户端与服务端请求管道的可组合单元在 tRPC 中Link 是用于定制 tRPC 客户端与服务端之间数据流的单元。一个理想的设计约束是一个 link 只做一件事这件事可以是对一次 tRPC 操作query、mutation或subscription做自包含的修改例如调整请求头、改写输入、重试失败请求或者基于这次操作产生的副作用例如记录日志、埋点上报。由于每个 link 只承担单一职责真实场景下通常需要把多个 link组合起来使用。tRPC 官方文档给出的典型例子是loggerLink负责打印请求与响应日志加httpBatchLink负责把多个请求合并成一个 HTTP 请求发送出去二者通过客户端配置中的links数组串联构成完整的请求处理流程。需要先说明下面示例以 Next.js 的createTRPCNext为例但同样的写法也可以直接套用在 vanilla tRPC 客户端createTRPCProxyClient上——它们都接收一个带links属性的配置对象见 官方相关示例目录。链接链Link Chain数组顺序与双向执行把多个 link 放进links数组并交给 tRPC 客户端得到的是一条link chain链接链。链接链的含义是发起请求时tRPC 客户端按照links数组中 link 的先后顺序依次执行它们处理响应时会按相反顺序再次经过这些 link。也就是说排在最前面的 link 最外层它先看到请求、最后拿到结果排在最后的 link 负责真正把请求送到服务器因此必须是一个终止 link。每次请求都会穿过整条链并原路返回。下图是该机制的官方示意图原图位于 www/static/img/links-diagram.svg概念源自 Apollo Client 的 link 架构最小可用配置在客户端接入 links在 Next.js 集成中你通常在config()里返回一个带links的对象例如utils/trpc.tsimport { httpBatchLink, loggerLink } from trpc/client; import { createTRPCNext } from trpc/next; export default createTRPCNextAppRouter({ config() { const url http://localhost:3000; return { links: [ loggerLink(), httpBatchLink({ url, }), ], }; }, });如果你使用的是 vanilla 客户端写法几乎一致——把createTRPCNext换成createTRPCProxyClientlinks数组的内容完全相同例如import { createTRPCProxyClient, httpBatchLink, loggerLink } from trpc/client; import type { AppRouter } from ../server; const client createTRPCProxyClientAppRouter({ links: [ loggerLink(), httpBatchLink({ url: http://localhost:3000 }), ], });执行过程解读loggerLink()在链最前请求上行时先记录日志httpBatchLink({ url })是链尾的终止 link它把操作真正发往服务端响应下行时按反序先回到httpBatchLink内部再流经loggerLink后者记录日志并携带elapsedMs耗时。若按反序把httpBatchLink放前面、loggerLink放后面日志打印与请求发送的相对顺序就会改变——理解数组顺序即执行顺序对排查链接链行为至关重要。Link 的类型本质从TRPCLink到OperationResultObservable官方文档对 link 的定义可以拆成三个嵌套层次而这三个层次在仓库源码的类型声明里有非常直接的对应——见 packages/client/src/links/types.ts// packages/client/src/links/types.ts节选 export type OperationLink TInferrable extends InferrableClientTypes, TInput unknown, TOutput unknown, (opts: { op: OperationTInput; next: ( op: OperationTInput, ) OperationResultObservableTInferrable, TOutput; }) OperationResultObservableTInferrable, TOutput; export type TRPCLinkTInferrable extends InferrableClientTypes ( opts: TRPCClientRuntime, ) OperationLinkTInferrable;对照这张类型定义可以把自定义 link 的三段式结构讲清楚第一层最外层工厂函数link 是一个返回函数、且形参为TRPCClientRuntime的函数。这个runtime参数由 tRPC 在创建客户端时传入通常用于在创建终止 link时传递运行时配置。如果你写的不是终止 link可以不声明任何参数直接写成一个返回中间层函数的函数——此时该 link 应当不加括号地放进links数组例如links: [..., myLink, httpBatchLink(...)]因为调用myLink这件事由 tRPC 客户端替你完成。第二层中间层中间件第一层返回的函数接收一个对象内含两个属性op客户端正在执行的Operation包含type、path、input、context、id、signal等字段next用于把操作交给链条中下一个 link的函数。第三层观察者订阅层第二层返回的函数最终返回由trpc/server/observable提供的observable。observable接收一个以observer为参数的回调link 通过这个observer向上游外层 link通知操作结果的去向——你可以原样return next(op)直通下游也可以订阅next的结果从而获得处理操作结果含响应值、错误、完成的能力。Operation与OperationResultEnvelope链接链中流动的数据契约为了让三层结构可操作需要先理解链接链中流动的对象长什么样。从 types.ts 可以看到一次操作Operation的完整字段export interface OperationContext extends Recordstring, unknown {} export type OperationTInput unknown { id: number; // 本次操作的唯一编号 type: mutation | query | subscription; // 操作类型 input: TInput; // 过程输入参数 path: string; // 过程路径例如 post.byId context: OperationContext; // 可读写的上下文对象跨 link 传递元数据 signal: MaybeAbortSignal; // 中止信号用于取消 };中间 link 之间通过next(op)传递的是Operation终止 link 在真正完成网络请求后向上返回的是 OperationResultEnvelope其中包含服务端的结果或连接状态以及可能被修改的context。从源码看客户端初始化时会对配置里的每个 link 执行一次外层工厂调用把整条链实例化好。见 packages/client/src/internals/TRPCUntypedClient.tsthis.runtime {}; // Initialize the links this.links opts.links.map((link) link(this.runtime));这正是文档中所说每个 link 的初始化在每个 app 中只发生一次的底层实现依据——后续每次请求只是复用这条已实例化的链。编写一个自定义 Link完整示例与逐步拆解下面是从官方文档原样保留的完整自定义 link 示例它用console.log打印每个操作与结果演示了观察并转发上游/下游流量的写法import { TRPCLink } from trpc/client; import { observable } from trpc/server/observable; import type { AppRouter } from server/routers/_app; export const customLink: TRPCLinkAppRouter () { // here we just got initialized in the app - this happens once per app // useful for storing cache for instance return ({ next, op }) { // this is when passing the result to the next link // each link needs to return an observable which propagates results return observable((observer) { console.log(performing operation:, op); const unsubscribe next(op).subscribe({ next(value) { console.log(we received value, value); observer.next(value); }, error(err) { console.log(we received error, err); observer.error(err); }, complete() { observer.complete(); }, }); return unsubscribe; }); }; };对照官方文档的三步法这段代码可以逐行理解为初始化阶段每 app 一次export const customLink: TRPCLinkAppRouter () {...}这一层只在应用初始化时执行一次。注释中特别提示这里很适合做进程级缓存等一次性初始化工作。因为它是非终止 link所以在links数组中直接写customLink不调用由客户端在构造时注入runtime。每请求阶段return ({ next, op }) {...}在每个操作进入该 link 时执行。op告诉你这次要做什么next让你把操作原样交给下一环。订阅与转发return observable((observer) {...})建立观察者回调。代码先打印performing operation然后next(op).subscribe({...})订阅下游结果并在回调里收到值value时打印并observer.next(value)继续向上游转发收到错误err时打印并observer.error(err)收到完成信号时observer.complete()。清理return unsubscribe把取消订阅函数交还给 observable 运行时保证操作被取消或链路中断时能正确释放订阅。这是可观察对象模式的典型资源回收点。什么时候该只转发什么时候该订阅如果你的 link 只是修改输入或给请求附加东西比如注入 header可以不必订阅next直接return next(op)把下游产生的 observable 原样返回让整条链保持透传。只有当 link 需要在结果返回时做额外处理如日志、缓存写入、错误重试、请求拆分时才需要next(op).subscribe(...)并在回调中把事件继续转发给observer。想找真实的参考实现官方文档建议如果需要一个更贴近真实场景的自定义 link 参考直接去看 tRPC 自带的一批内置 link 源码。它们就在本仓库的 packages/client/src/links/ 目录下包括httpLink.ts最基础的 HTTP 终止 link一次操作一个请求httpBatchLink.ts支持批量的 HTTP 终止 linkhttpBatchStreamLink.ts基于流式响应的批量链接httpSubscriptionLink.ts 与 wsLink/处理订阅类操作的链接loggerLink.ts内置日志链接splitLink.ts 与 retryLink.ts条件分流与自动重试。其中loggerLink是实现副作用型 link的最好范本——它并不修改请求而是在请求上行时与结果下行时分别调用日志函数。从源码 loggerLink.ts 可以看到其内部正是通过next(op).pipe(tap({ next, error }))观察下游事件后再.subscribe(observer)完成转发。终止 LinkTerminating Link链接链的最后一环终止 link 是链接链中最后一个 link。与普通 link 不同终止 link不调用next——因为链条到这里就到头了它负责真正把组装好的 tRPC 操作发送给 tRPC 服务端并把响应包装成OperationResultEnvelope返回给上游。两个必须记住的硬性约束客户端配置里的links数组至少要有一个 link这个 link或数组中最后一个 link必须是终止 link。如果数组末尾不是终止 linktRPC 操作将永远无法送达服务端——请求会在链中被吞掉。内置的终止 link 怎么选tRPC 官方推荐使用httpBatchLink作为默认终止 link其余终止 link 包括httpLink与wsLink。它们各自适用不同的场景终止 link请求行为典型场景httpBatchLink把同一事件循环 tick 内发起的多个操作合并成一个 HTTP 请求批量默认首选减少请求数量httpLink每次操作单独发一个 HTTP 请求需要单独请求、逐请求取消等场景wsLink通过 WebSocket 发送操作需要真正的服务端订阅、推送场景从源码看httpBatchLink的批处理细节以官方推荐的httpBatchLink为例其实现位于 httpBatchLink.ts。从中可以看到几个值得了解的内部行为它内部为query与mutation各维护了一个 dataLoader 批处理器把同一个 tick 内到达的多个操作合并为一次请求提供了maxURLLength与maxItems两个上限选项默认值均为Infinity见 httpBatchLink.ts用于在 URL 过长或批内条目过多时回退到不合并的逐条发送subscription类型的操作会被显式拒绝并抛出错误提示改用httpSubscriptionLink或wsLink见 httpBatchLink.ts。也就是说查询与变更类操作适合批量而订阅是长连接语义必须走专门的订阅链路。这也是为什么splitLink、wsLink这类链接链组合在真实项目中非常重要。管理上下文Context跨 link 传递元数据当一次操作沿链接链流动时它携带一个上下文对象context链上的每个 link 都可以读取和修改它。这样链路中靠前的 link 可以把元数据放进 context供后面其他 link 在执行逻辑时使用——例如标记这个请求不要走批量。用法要点读取/修改当前上下文通过op.context拿到当前操作的上下文对象并直接修改例如op.context.foo bar设置初始值在一次具体操作开始时设置 context 的初始值方法是给query/useQuery或mutation、subscription等的调用传入context参数。实战案例对特定请求禁用批处理官方文档在链接到 splitLink 一节时给出了 context 最经典的用途——按需禁用批量请求。假设你的客户端一直使用httpBatchLink批量开启。若某个请求必须单独发送例如涉及超大 body、需要独立缓存或希望该请求独立失败重试可以在配置中用splitLink根据 context 动态切换终止 linkimport { createTRPCProxyClient, httpBatchLink, httpLink, splitLink, } from trpc/client; import type { AppRouter } from ../server; const url http://localhost:3000; const client createTRPCProxyClientAppRouter({ links: [ splitLink({ condition(op) { // 检查上下文中的 skipBatch 标记 return op.context.skipBatch true; }, // 条件为真时走单请求 true: httpLink({ url, }), // 条件为假时继续批量 false: httpBatchLink({ url, }), }), ], });splitLink本身也是一个 link实现见 packages/client/src/links/splitLink.ts它的condition接收op并返回布尔值true/false两个分支各接收单个 link 或 link 数组且每个分支都必须以终止 link 收尾。需要注意当你给splitLink传 link 时它会基于你传入的链接新建一条完整的链接链所以只给一个 link 时必须给终止 link给多个 link 时要把终止 link 放在分支数组末尾。然后在调用方为单次操作设置context.skipBatchconst postResult proxy.posts.query(null, { context: { skipBatch: true, }, });如果你在用 React hooks则把 context 放进trpc.context官方文档使用的 v10 写法export function MyComponent() { const postsQuery proxy.posts.useQuery(undefined, { trpc: { context: { skipBatch: true, }, }, }); return ( pre{JSON.stringify(postsQuery.data ?? null, null, 4)}/pre ); }这个例子完整展示了 tRPC context 机制的三个要素初始值来自调用方 →op.context在链上可见 → 后续 link这里是splitLink的condition据此决定行为。它也是链接链上上游决策、下游执行协作模式的教科书级用法。总结tRPC 的 Links 机制把客户端如何发送请求、如何处理响应抽象成一条高度可组合、可观测、可扩展的链接链每个 link 只做一件事多个 link 通过links数组按序组合请求时正向执行、响应时反向流动一个 link 本质上是 types.ts 中定义的嵌套函数类型——外层工厂每客户端初始化一次、中间层中间件每请求执行、内层 observable建立订阅并向上游转发事件链条必须终止于终止 linkhttpBatchLink为官方首选其次httpLink、wsLink否则操作不会真正发出通过op.context可以在链上读写元数据配合splitLink即可实现按请求禁用批量这类精细控制。如果想继续深入推荐在本仓库继续阅读以下关联章节httpBatchLink 详解 与 httpLink 详解两个最常用终止 link 的完整选项说明wsLink 详解WebSocket 订阅链路loggerLink 详解日志链接的全部可选项含enabled/console/colorMode等splitLink 详解条件分流httpBatchStreamLink 详解流式批量内置链接源码目录 packages/client/src/links/以及客户端源码入口 packages/client/src/internals/TRPCUntypedClient.ts可进一步印证链接链的初始化与调用细节【免费下载链接】trpc♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考