ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

tRPC 路由拆分与合并完全指南:子路由嵌套、mergeRouters 与 lazy 按需加载

tRPC 路由拆分与合并完全指南:子路由嵌套、mergeRouters 与 lazy 按需加载 tRPC 路由拆分与合并完全指南子路由嵌套、mergeRouters 与 lazy 按需加载【免费下载链接】trpc‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc本文将系统讲解 tRPC 服务端 router 的三种组织形态把功能拆成独立子路由后用嵌套方式组合、用t.mergeRouters合并出扁平命名空间以及用lazy()实现路由的按需动态加载以降低应用冷启动成本。读完你不仅会写出清晰可维护的多文件路由结构还能理解合并与懒加载在 trpc/server 内部究竟如何工作。随着接口数量增长把所有 API 代码写在同一个文件里会变得难以维护原文语。tRPC 提供了多套互补的组合机制让你既能按业务模块拆分代码又能保持端到端类型安全。本文以 merging-routers 官方文档为骨架结合 packages/server 源码与 examples/lazy-load 完整示例逐层展开。基础从统一的 tRPC 实例导出构建工具拆分路由的前提是一个后端只初始化一次initTRPC然后从这唯一实例导出router、publicProcedure等构建工具供所有子路由文件复用关于初始化与创建 router 的基础可参见 routers.md 与 procedures.md// trpc.ts import { initTRPC } from trpc/server; const t initTRPC.create(); export const router t.router; export const publicProcedure t.procedure; export const mergeRouters t.mergeRouters;从源码看initTRPC.create()返回的对象同时挂载了router、mergeRouters、procedure、middleware、createCallerFactory等构建单元见 initTRPC.ts。router与mergeRouters的注释都指向merging-routers文档说明拆分合并正是这两个 API 的核心职责。后续所有子路由都从../trpc导入同一批工具从而保证配置、errorFormatter、transformer 的一致性。方式一把子路由作为命名空间嵌套进根路由第一种拆分思路是按模块建路由再按命名空间挂在根路由上。先分别定义两个子路由例如用户模块与文章模块// routers/user.ts import { router, publicProcedure } from ../trpc; export const userRouter router({ list: publicProcedure.query(() { // [..] return []; }), });// routers/post.ts import { router, publicProcedure } from ../trpc; import { z } from zod; export const postRouter router({ create: publicProcedure .input( z.object({ title: z.string(), }), ) .mutation((opts) { const { input } opts; // [...] }), list: publicProcedure.query(() { // ... return []; }), });然后在根路由文件里把这两个子路由作为普通值传给router()用键名充当命名空间// routers/_app.ts import { router } from ../trpc; import { userRouter } from ./user; import { postRouter } from ./post; const appRouter router({ user: userRouter, post: postRouter, }); appRouter.user // ^? 类型提示嵌套的 user 命名空间下的全部 procedure appRouter.post // ^? 类型提示嵌套的 post 命名空间下的全部 procedure export type AppRouter typeof appRouter;嵌套方式背后的展平逻辑嵌套并不产生真正的路由器对象套路由器而是被router()内部结构性地展平。查看 router.ts 中的类型定义可以发现当一个选项值$Value extends Routerany, infer TRecord时它会被解包为TRecordprocedure 则保留自身。因此router({ user: userRouter })产生的 record 结构形如{ user: { list: procedure } }。运行时的step()递归router.ts会把所有 procedure 以key1.key2...的点号路径登记进统一的_def.procedures注册表如user.list、post.create这也是 HTTP 请求中 procedure 路径的来源。可以推断客户端调用时就会呈现出与命名空间一致的层级trpc.user.list、trpc.post.create。同时 router.ts 规定then、call、apply是保留字不能用作 router 或 procedure 的名字因为then会破坏 Promise 化、call/apply会影响函数调用语义。方式二用t.mergeRouters合并成单层扁平命名空间如果你更希望所有 procedure 都平铺在同一个顶层命名空间下可以使用t.mergeRouters。注意子路由内部的过程名此时必须自解释如userList、postCreate因为合并后不再有父级命名空间来区分归属// routers/user.ts import { router, publicProcedure } from ../trpc; export const userRouter router({ userList: publicProcedure.query(() { // [..] return []; }), });// routers/post.ts import { router, publicProcedure } from ../trpc; import { z } from zod; export const postRouter router({ postCreate: publicProcedure .input( z.object({ title: z.string(), }), ) .mutation((opts) { const { input } opts; // [...] }), postList: publicProcedure.query(() { // ... return []; }), });// routers/_app.ts import { mergeRouters } from ../trpc; import { userRouter } from ./user; import { postRouter } from ./post; const appRouter mergeRouters(userRouter, postRouter); // ^? 类型提示合并后平铺的 procedure 集合 export type AppRouter typeof appRouter;运行时发生了什么配置协商与冲突检测t.mergeRouters的实现位于 router.ts其关键行为如下记录合并先取每个参与合并路由的_def.record通过mergeWithoutOverrides合并。该工具函数utils.ts在遇到重复键且值不同时会直接抛出Duplicate key key从而防止合并后过程名冲突被静默吞掉若两侧是同一个引用则允许通过。这一规则对应文档中需要为 procedure 起全局唯一名字的隐含约束。errorFormatter 协商逐项检查参与者的 errorFormatter若出现多个互不相同且非默认的 formatter抛错You seem to have several error formatters。transformer 协商逻辑与 formatter 一致冲突时抛错You seem to have several transformers。这意味着参与合并的各 router 最好来自配置相同的 tRPC 实例。合并结果重新走createRouterFactory把mergeWithoutOverrides得到的新 record 交给路由工厂重建出一个全新的BuiltRouter因此合并产物的类型与运行时都和一个原生创建的 router 完全一致isDev、isServer、allowOutsideOfServer取所有参与者的与。以上合并与冲突行为都有对应测试用例佐证见 router.mergeRouters.test.ts其中验证了正常合并后caller.foo()/caller.bar()都能调用、仅一方带自定义 formatter/transformer 时可成功合并而双方各带不同 formatter 或 transformer 时分别抛出上述两条错误。两种方式怎么选嵌套router({ user, post })保留业务命名空间procedure 天然分组路径带前缀适合模块间边界清晰的团队协作客户端调用是user.list这类层级路径。mergeRouterst.mergeRouters所有 procedure 平铺在单层命名空间路径更短但要求过程名全局不重复。两者的类型都是完全端到端推导的写法偏好而已不影响客户端类型安全。方式三lazy动态加载子路由如果某些路由体积大、初始化开销高可以用lazy把它们延迟到首次被访问时才加载。这在减少应用冷启动开销时很有用原文语reduce cold starts懒加载完成之后路由的使用方式与普通路由没有任何区别。lazy从trpc/server顶层导出见 trpc/server/index.ts是服务器包面向用户的公开 API。沿用前文的初始化代码先定义两个普通的子路由// routers/greeting.ts import { router, publicProcedure } from ../trpc; export const greetingRouter router({ hello: publicProcedure.query(() world), });// routers/user.ts import { router, publicProcedure } from ../trpc; export const userRouter router({ list: publicProcedure.query(() [John, Jane, Jim]), });接着在根路由中用lazy包装动态import()// routers/_app.ts import { lazy } from trpc/server; import { router } from ../trpc; export const appRouter router({ // Option 1: 模块恰好只导出 1 个 router 时的简写 greeting: lazy(() import(./greeting.js)), // Option 2: 模块导出多个 router 时用 .then 指明取哪一个 user: lazy(() import(./user.js).then((m) m.userRouter)), }); export type AppRouter typeof appRouter;lazy 的判定规则lazy的函数签名与运行逻辑见 router.ts。加载函数被调用后若importRouter()直接解析出一个 router例如用了.then((m) m.xxxRouter)直接返回它否则把模块视为导出表Object.values后要求恰好只有 1 个导出且该导出是 router否则抛出错误Invalid router module - either define exactly 1 export or return the router directly。因此 Option 1 的简写成立的前提正是模块文件里只export了一个 router。懒加载在框架内部如何生效在 router.ts 的step()里isLazy(item)的值不会立即加载而是被登记进_def.lazy键为完整点号路径并生成一个createLazyLoader其load()用once()做了记忆化router.ts保证模块只会被真实加载一次加载完成后把该模块的 record 递归合并进总 record并把其内部嵌套的懒加载项继续注册。真正按需触发的地方是getProcedureAtPathrouter.ts当某个点号路径在_def.procedures中找不到时它会查找第一个匹配该路径前缀的 lazy 键await lazyRouter.load()后再查一次。由于HTTP 分发路径与createCaller服务端直调都经由getProcedureAtPath解析 procedure见 router.ts 与 router.ts可以推断无论请求来自网络还是同一进程内的服务端调用懒加载对两者都一致生效。类型层面AppRouter typeof appRouter的推导依赖lazy的泛型参数故客户端拿到的是完整类型而无需感知懒加载。仓库中的完整可运行示例见 examples/lazy-load其 routers/_app.ts 用lazy同时挂载了user与slow模拟慢速模块两个子路由trpc.ts 负责初始化与导出适合作为对照实现的参考。使用注意事项lazy依赖运行时动态import()在打包环境中应配合支持代码分割code-splitting的构建器使用才能把子路由拆成独立 chunk 并在真需要时才发起加载。懒加载之后的路由调用方式与普通路由完全一致文档明确说明 no difference因此对调用方透明迁移成本低。若一个模块导出多个 router必须用.then((m) m.routerName)显式指定否则会触发上文提到的校验错误。小结拆分布局不是玄学而是由清晰的 API 支撑的结构化实践组合手段结果形态适用场景类型/运行约束router({ user: userRouter, post: postRouter })带命名空间的层级路径模块边界清晰、希望路径自带归属procedure 路径为namespace.method避免保留字键名t.mergeRouters(r1, r2, ...)单层扁平命名空间希望所有过程平铺、短路径顶层过程名不得重复冲突的 formatter/transformer 会抛错router({ x: lazy(() import(...)) })按需加载的子路由降低冷启动/首包体积模块需恰好导出 1 个 router或用.then显式挑选具体到实现层面三种方式的底层都收敛到 router.ts 中的同一套record 点号路径注册表模型嵌套与mergeRouters只是以不同方式组装 recordlazy则把 record 的展开推迟到首次访问。理解这一点后你可以放心地按照业务模块拆分文件、自由组合命名空间同时保持 tRPC 一贯的端到端类型安全。【免费下载链接】trpc‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表