尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Civitai tRPC v10 → v11 与 React Query v5 升级实战:从 2104 个类型错误到 92 个懒加载 Router 的全量迁移指南

Civitai tRPC v10 → v11 与 React Query v5 升级实战:从 2104 个类型错误到 92 个懒加载 Router 的全量迁移指南 Civitai tRPC v10 → v11 与 React Query v5 升级实战从 2104 个类型错误到 92 个懒加载 Router 的全量迁移指南【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai本文是 docs/trpc-v11-migration-plan.md 的展开讲解配套原理文档为 docs/trpc-router-dev-recompile-and-v11-notes.md。Civitai 仓库在next dev下编辑任意src/server/services/*文件都会触发整个/api/trpc/[trpc]路由的重编译约 6,255 个模块、冷启动约 21s / 热重建约 8.8s根因是pages/api/trpc/[trpc].ts→appRouter静态导入全部 ~93 个 Router 及其全部 Service形成一个巨型编译单元。本指南完整记录该仓库从 tRPC v10 React Query v4 迁移到 tRPC v11 React Query v5 的实测过程级联错误根因、机械改写、语义差异验证、循环依赖审计以及最终把 92 个 Router 全部切换为router.lazy()的分阶段执行方案。迁移的起点一次升级两个必须同时落地的依赖tRPC v11 的 API 变动其实是整场迁移中最小的部分。真正的成本在于 v11 强制绑定的tanstack/react-queryv4 → v5 升级——trpc/react-queryv11 的 peer dependency 是 React Query v5而仓库此前钉在^4.12.0。二者无法拆分发布trpc/react-query10固定依赖 RQ v4所以 RQ v5 和 tRPC v11 必须进入同一个 PR / 同一次版本 bump。而这一切背后的动机是开发体验v11 的router.lazy()能把每个 Router 及其 Service 子图拆成独立 chunk编辑某个 Service 时只重建对应 chunk而不是整条路由。这是一个可选、独立的第三阶段只有在 API 迁移全绿之后才进行。迁移前的环境检查结论从仓库现状看均满足无需额外 bumpReact 18.3.1 / Next 14.2 满足 RQ v5 的 React 18 下限TypeScript 5.9.2 满足 tRPC v11 的5.7.2要求。需要一起升的四个包当前均为^10.45.0trpc/server、trpc/client、trpc/next、trpc/react-query。决定性的发现2104 个错误只是 3 个类型注解的级联首次 typecheck 报了2,104 个错误约 89% 是隐式any。这并非 2104 个独立问题——src/utils/trpc.ts 中三处破损的类型注解让整个trpcproxy 解析成了any于是 1,000 调用点的每个回调参数全部变成隐式any错误。修好这三行后错误数直接降到476CreateTRPCNextAppRouter, NextPageContext, null→ v11 只接受2 个泛型而不是 3 个TS2314。类型CreateTRPCProxyClient改名为CreateTRPCClient运行时createTRPCProxyClient也随之改为createTRPCClientTS2724。该仓库已采用别名写法见 src/utils/trpc.ts 中的export const trpcVanilla: CreateTRPCClientAppRouter createTRPCClientAppRouter({...})。transformersuperjson从根配置移到了每个httpLink上。其余根级 / 配置级修复小而高杠杆createCallerFactory不再是trpc/server的导出——改为从t上解构export const { router, middleware, createCallerFactory } t然后调用createCallerFactory(appRouter)。仓库中已落地于 src/server/trpc.ts。中间件里的options.rawInput→await options.getRawInput()现在异步。已在 src/server/trpc.ts 落地并注释// v11: rawInput became the async getRawInput()相关测试见 src/server/routers/tests/model.router.edge-cache-chain.test.ts。类型DefaultErrorShape→TRPCDefaultErrorShape涉及 6 个文件。Worktree 陷阱event-engine-common/是一个git submodule全新 worktree 里它是空的导致image.service.ts无法解析需要git submodule update --init或从主分支拷贝过来。一个值得记录的坑官方 RQ v5remove-overloadscodemod 在本仓库是空操作——它只改写裸tanstack/react-query多参调用而本仓库的 hooks 走的是 tRPC 自己的(input, opts)签名。codemod 只产生了 recast 重排噪音还包括对非 RQ 文件的注释打乱已回退。实测爆炸半径1,054 个调用点的改造清单迁移计划给出了一张基于当前代码树的量化影响表这是评估工作量与风险的第一手依据表面数量影响trpc.*.useQuery/useMutation/useInfiniteQuery调用点~1,054v5 类型变化落点trpc/*包^10.45.04server、client、next、react-query一起升src/server/routers/index.ts 静态导入的 Router92Phase 3lazy()目标useQuery/useInfiniteQuery内部的onSuccess/onError~66⚠️v5 已移除——需手工重写无 codemod是主要人工工作onSuccess/onError/onSettled/keepPreviousData全部~532mutation 回调保留query 回调移除.isLoading用法~230queryisLoading保留重定义只有mutation的isLoading→isPendinguseInfiniteQuery调用点~41✅几乎零改动——tRPC 包装了initialPageParamcacheTime~42→gcTimecodemodisInitialLoading~66→isLoadingcodemodisPreviousData~5→isPlaceholderDatacodemodkeepPreviousData: true子集→placeholderData: keepPreviousDatacodemodstatus loading比较~22→pending半手工codemod 覆盖大部分Hydrate/dehydrate~6Hydrate→HydrationBoundary需核对 SSG/SSR 用法.remove()查询方法≤6已移除 →queryClient.removeQueriesrefetchInterval:回调~82 参(data, query)→ 单参(query)服务端中间件rawInput1→getRawInput()createTRPCProxyClient2→createTRPCClient别名纯装饰性直接tanstack/react-query导入~20对照 v5 API 审查useQueries1新的{ queries: [...] }对象形式useErrorBoundary/isDataEqual/inferHandlerInput/ProcedureArgs0✅ 未使用trpc.useContext()0✅ 别名永久保留——非问题机械批量改写476 → 86380×mutation.isLoading→.isPendingquery 的.isLoading在 v5 中仍然存在所以被标记的恰好都是 mutation通过基于 tsc 报告的列定位脚本批量应用。4×.isPreviousData→.isPlaceholderData。剩余 86 个错误的实质内容cacheTime→gcTime约 42 处——纯改名但注意仅限客户端服务端有自己的cacheTime变量不能误改。keepPreviousData: true→placeholderData: keepPreviousData函数导入。⚠️ 本仓库反复出现的惯用法是wrapper 接受options?: { keepPreviousData?: boolean }并展开进 hook约 50 个文件。最终选择方案 B在 src/hooks/trpcHelpers.ts 增加共享的withPlaceholderData()辅助函数在展开点把布尔值翻译成 v5 形式直接字面量则用tanstack/react-query的placeholderData: keepPreviousData。query 级onSuccess/onError/onSettled在 v5 移除10 处——改写为对data/error的useEffect手动 refetch 场景改为await refetch()。杂项refetchInterval回调现在接收 Query用query.state.data、useIsMutating(key)→useIsMutating({ mutationKey })、getNextPageParam的: 0哨兵 →: undefined、user可能为 undefined 的守卫收窄。确认的非问题无限查询tRPC 包装了initialPageParamgetNextPageParam零错误、useContext/ utils API不变。已验证的 v5/v11 语义编辑前必读query 的isLoading并没有被移除。v5 把status: loading改名为pending并新增isPending“还没有数据”。isLoading依然存在但被重定义为isPending isFetching。因此约 230 处 query.isLoading大多照常工作——除了 disabled/paused 查询enabled: falsev4 中isLoading为truev5 中变成false此时isPending为true。需要专门审计enabled: false spinner 逻辑。对mutation而言isLoading确实被移除 → 必须改成isPending。无限查询几乎零改动。tRPC 的useInfiniteQuerywrapper 替你注入initialPageParam你只需可选传入initialCursor。现有getNextPageParam: (lastPage) lastPage.nextCursor不变。不要手动给约 41 个站点加initialPageParam。useContext/ utils proxy 不变。useContext被“可预见的将来”别名保留setInfiniteData/getInfiniteData/invalidate行为一致零工作。transformer 移到 link。客户端transformer: superjson从根配置移出进入每个httpLink/httpBatchLink。createTRPCNext按 v11 指南仍接受顶层 transformer但 proxy/vanilla 客户端只能在 link 上。服务端保留在initTRPC.create。仓库源码里的 v11 落地形态客户端src/utils/trpc.ts迁移后的客户端配置清晰体现了 v11 的全部三个关键点createTRPCNext两个泛型 顶层 transformerexport const trpc: CreateTRPCNextAppRouter, NextPageContext createTRPCNextAppRouter({...})且transformer: clientTransformer作为顶层选项其WithTRPCOptions与TransformerOptions相交。注释明确说明CreateTRPCClientOptions.transformer是forbidden sentinel——vanillacreateTRPCClient只能在 link 上带 transformer。transformer 同时出现在每个 link 上承载真正的线上反序列化httpLink({ transformer: clientTransformer, url, ... })、httpBatchStreamLink({ transformer: clientTransformer, ... })。v11 保留不变的幸存者属于 link/fetch 层未受 v11 影响largeFetch大查询 POST 路径httpLinkWithLargeQuerySupportisLargeQuery、authedCacheBypassLink、queryClientProxy 单例与每请求服务端 QueryClient、x-trpc-method-overridePOST 转换路径。isTooLargeToBatch的URL_INPUT_BUDGET 1800编码预算逻辑也原样保留。RQ v5 的queryRetry策略batch 开启时 0 次重试一次批传输失败等于 N 个查询同时失败retry: 1会触发 N 路重试风暴未开启时failureCount 1即一次重试。服务端src/server/trpc.tsexport const { router, middleware, createCallerFactory } t——createCallerFactory从t解构。transformer 仍保留在initTRPC.create({ transformer: buildTransformer(...) })且自定义的withSpan(trpc:serialize:superjson/trpc:serialize:devalue, ...)序列化器包装必须原样搬移——计划文档特别警告“我们的自定义 withSpan 包装序列化器必须完整移动”仓库现状证实它确实完整保留还叠加了instrumentSerialize冻结埋点。中间件applyDomainFeature使用await options.getRawInput()异步。机械改写的落点src/hooks/trpcHelpers.ts这是“方案 B”的实体withPlaceholderData()把调用方仍在用的旧式keepPreviousData?: boolean翻译成 v5 的placeholderData函数形式其余选项原样透传并 re-exportkeepPreviousData作为单一导入源export function withPlaceholderDataT extends { keepPreviousData?: boolean }( options?: T ): OmitT, keepPreviousData { placeholderData?: typeof keepPreviousData } { const { keepPreviousData: keep, ...rest } options ?? ({} as T); return { ...(rest as OmitT, keepPreviousData), ...(keep ? { placeholderData: keepPreviousData } : {}), }; }典型用法useInfiniteQuery(input, withPlaceholderData(options))覆盖了约 50 个“wrapper 展开 options 进 hook”的站点。风险排序哪些地方会悄悄出错query 级onSuccess/onError/onSettled移除约 66 处。这些是静默行为变更——很多不会表现为类型错误。必须手工改写移到useEffect、从data推导、或用全局QueryCache回调。这是最容易出错的一项没有 codemod 覆盖。lazy()暴露循环依赖仅 Phase 3。切换 Router 到lazy()会重排模块求值顺序把潜伏的循环依赖在运行时暴露成TypeError: Cannot read properties of undefined (reading optional|union|...)发生在 schema/DataGraph 顶层。缓解方式是把修循环的 pass 前置Phase 0。类型检查长尾。1,054 个调用点意味着推断偏移pnpm run typecheck是第一遍真实的测试工具。superjson transformer 搬迁。从根客户端配置移入 link服务端保留在initTRPC.create。局部但容易出细微错误——自定义的withSpan包装序列化器必须完整搬移。分阶段计划四步走Phase 0 — 提前去风险动版本之前先做与升级无关、纯赚且是 Phase 3 的前置条件用CIRCULAR_DEPENDENCY_PLUGINtrue跑 dev/buildnext.config.mjs 已接线该环境变量与circular-dependency-plugin。记录每个报出的循环。修复方式把共享叶子值常量/枚举/schema抽到零导入的叶子模块并从源头导入叶子 schema而不是经由大模块 re-export——这正是 notes 文档 §1 的模式。退出标准插件报告零循环或一份已知、文档化的 allowlist。Phase 1 — React Query v4 → v5工作量主体理想情况是在 tRPC v10 上先做——但不行v10 钉死 RQ v4所以 RQ v5 与 tRPC v11 必须同 PR。编辑顺序上先把代码改成 RQ-v5 形状阶段末尾再一起翻转两个依赖一次性把tanstack/react-query升到^5、四个trpc/*升到^11。跑官方 RQ v5 codemod 清机械改动cacheTime→gcTime约 42、mutationisLoading→isPending约 230 的子集——codemod 是类型感知的验证它不动 queryisLoading、keepPreviousData: true→placeholderData: keepPreviousData函数导入。无 codemod 的手工工作重写约 66 处 query 上的onSuccess/onError/onSettleduseInfiniteQuery补必需initialPageParam与类型化getNextPageParam把 1 个useQueries调用改成{ queries: [...] }对象形式审查约 20 个直接tanstack/react-query导入。退出标准pnpm run typecheck全绿、应用可启动、冒烟测试关键 feed图片 feed、generation、模型页。Phase 2 — tRPC v11 API 迁移配置局部化主体集中在两个文件src/utils/trpc.ts 与 src/server/trpc.tstransformer 移到 link客户端transformer: superjson从根createTRPCNext/createTRPCClient配置移进httpLink/httpBatchLink服务端保留在initTRPC.create。withSpan(trpc:serialize:superjson, ...)序列化器包装完整搬移。确认幸存者link/fetch 层不受 v11 影响largeFetch、authedCacheBypassLink、queryClientProxy 单例 每请求服务端 QueryClient、x-trpc-method-overridePOST 转换路径。trpc/server/adapters/next的createNextApiHandler在 v11 中仍存在——[trpc].tshandler 无需改动。本阶段 Router 保持eager。退出标准typecheck 全绿 eager Router 应用可启动。这是“v11 已上线、行为零变化”的检查点。Phase 3 —router.lazy()转换开发提速的兑现增量进行仅在 Phase 0–2 全绿合并后进行把 src/server/routers/index.ts 中的 Router 转换为router.lazy(() import(./x.router))最重的先来image、model、post、generation、orchestrator。每转换一个用 notes 文档的诊断法验证编辑该 Router 触达的某个 Service确认没有○ Compiling /api/trpc/[trpc]行出现。警惕运行时undefined签名循环错误风险 #2——出现即说明 Phase 0 漏了潜伏循环修边缘不要掩盖。设定预期懒加载 Router 消除的是额外的路由重建热态约 8.8s不是 webpack re-seal 下限热态约 4.4s。该阶段可安全增量可按批次跨多个 PR 逐个落地。Phase 3 执行实录92 个 Router 全量懒加载仓库现状显示 Phase 3 已经完成src/server/routers/index.ts 中全部92 个 Router均已转换为lazy(() import(path).then((m) m.router))形式Router 文件保留具名导出lazy()重载接受() PromiseTRouter单文件变更。类型检查保持干净lazy()通过DecorateCreateRouterOptions的LazyRouter…分支保留AppRouter类型推断客户端类型不会坍缩最后一行export type AppRouter typeof appRouter照常工作。循环审计文档化的危险区懒加载重排模块求值顺序可能把潜伏的循环依赖暴露为运行时TypeError: Cannot read properties of undefined。next.config.mjs 中现有的circular-dependency-plugin块只扫描客户端 bundle!options.isServer会漏掉这里真正关键的服务端循环。静态审计12 个 agent、全部 92 个 Router发现11 个唯一导入循环86 个 Router 无循环。11 个循环在懒加载下全部无害——最初被标记“高”的两个是手工核验过的generation.schema ↔ generation.constants——反向边是import type运行时被擦除→ 无运行时循环 →generationSamplers在 schema 展开它之前完成初始化。image.service ↔ user.service——真实的运行时循环但两侧都不在模块顶层读取跨导入deleteImageById在函数体内使用getBasicDataForUsers/getCosmeticsForUsers/getProfilePicturesForUsers同理。两种求值顺序下都安全。其余 9 个中/低危stripe↔buzz↔user、research.router↔research.webhooks以及若干image.service↔{post,report,collection,cosmetic,new-order,tagsOnImageNew}.service都是仅函数体的循环——目前无害但是潜伏隐患这些模块日后若在模块作用域添加读取跨导入的代码就会在懒求值下抛错。修复模式就是已经确立的范式src/shared/data-graph/generation/version-ids.ts把共享值抽到零导入叶子模块。当前未应用——超出懒加载转换范围且对正确性无必要。无需任何循环修复。合并前仍建议做一次运行时冒烟测试启动 跑重路由——图片 feed、模型、generation、comics、stripe因为静态分析无法替代真正逐个加载每个懒 chunk。PR 拆分建议与工作量估计PR 拆分PR APhase 0只做循环依赖修复。可独立合并无版本 bump。PR BPhase 12RQ v5 tRPC v11 一起二者耦合Router 保持 eager。这是大头。PR C..nPhase 3每批lazy()转换一个 PR最重的 Router 优先。工作量关键路径总计约 4–6 个专注日Phase 3 随后陆续跟进Phase 00.5–1.5 天取决于插件暴露多少循环。Phase 13–4 天——大头是 66 个 query 回调重写 验证 1,000 站点。Phase 2约 0.5 天配置局部化。Phase 3每批约 0.5 天分布推进单步风险低。结论typechecker 扛起 Phase 1–2 的主体query 回调移除与 Phase 3 的循环错误才是需要人眼、而非编译器的地方。pnpm run typecheck基线 0 错误与pnpm run lintprettier:write是收尾双闸运行时冒烟测试onSuccess→useEffect重写、applyDomainFeature中间件getRawInput()是合并前的最后一道验证。【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表