tRPC Next.js App Directory 实验适配器:RSC、Server Actions 与缓存的一体化实战指南
【免费下载链接】trpc🧙♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc
本篇围绕 tRPC 仓库中的实验性示例 examples/.experimental/next-app-dir,讲解如何在 Next.js App Directory 中以"同一套 API 调用方式"同时支持 Server Components(RSC)、Server Actions 与客户端组件,并理解其基于react-server模块条件拆分入口、nextCacheLink缓存与revalidate失效机制的实现细节。读完本文,你可以复现该实验架构的完整搭建步骤,并判断它在生产环境中的适用边界。
一、这是什么:一个官方适配器的 Playground
该示例的定位在 README 中开宗明义:这是一个用于验证"tRPC + Next.js App directory 官方适配器"的 playground 仓库,并明确标注 "This is experimental and is subject to change"(实验性质、接口随时可能变化)。
README 中同时给出了一条重要背景说明:在正式适配器落地之前,App Directory 下已有两条可用的既有路径:
- 直接在组件中使用
@trpc/client(RSC 与非 RSC 组件均可); - 在客户端组件中使用
@trpc/next。
而本实验要验证的是第三条路:一个针对 App Directory 专门设计的客户端/服务端调用层,让开发者"no matter if you are in a server component"(无论身处服务端组件还是客户端组件)都能以相同方式使用 tRPC。
README 中的进度清单如实反映了当前成熟度:
| 事项 | 状态 |
|---|---|
| RSC 支持的概念验证(PoC) | 已完成 |
| Server Actions 的概念验证(PoC) | 已完成 |
| 缓存(caching)实现 | 已完成 |
| 服务端调用触发的缓存失效 | 未完成 |
| 客户端调用触发的缓存失效 | 未完成 |
| 服务端与客户端调用的双向失效联动 | 未完成 |
| API 最终定型、重测试 | 未完成 |
对应地,README 也给出了明确的生产环境警告:"Don't use this in production unless you are okay with large refactoring."——除非你能接受大规模重构成本,否则不要在生产中使用。这个警告在评估任何实验性方案时都应当放在第一位。
二、总体架构:一个本地 tRPC 包,两种运行时入口
实验的核心思路(README 的 Overview 章节)可以概括为两句话:
- 创建一个tRPC API handler(路由处理器);
- 创建一个本地 tRPC 包,为
"use client"与"use server"场景提供不同的 entrypoint。
第二个步骤是整个架构的枢纽:Next.js 打包时会根据模块是否带'use client'标识选择不同的模块条件(module condition)。该示例通过一个通过 pnpm 链接进应用包的本地包trpc-api实现了这种分流。
2.1 用 exports 条件区分 RSC 与客户端入口
包声明见 src/trpc/package.json:
{ "name": "trpc-api", "typings": "client.ts", "exports": { ".": { "types": "./client.ts", "react-server": "./server-invoker.ts", "default": "./client.ts" } } }关键点:
react-server条件命中时(即无'use client'的服务端组件 / Server Components 环境),解析到 server-invoker.ts;- 其余情况(客户端组件)走
default分支,解析到 client.ts。
应用侧则在 package.json 中通过"trpc-api": "link:./src/trpc"将该本地包以 workspace link 方式引入,与next@^15.3.8、react@^19.1.0、@tanstack/react-query@^5.80.3等依赖共同工作。这样,同一个import { api } from 'trpc-api'语句,在 RSC 与客户端组件中会拿到两个不同的客户端实现,但暴露相同的调用形态——这正是 README 所承诺的"same way"。
2.2 API Handler:标准 fetch 适配器
README 的第一步"Create an API handler for tRPC"对应 src/app/api/trpc/[trpc]/route.ts:
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'; import { createContext } from '~/server/context'; import { appRouter } from '~/server/routers/_app'; const handler = (req: Request) => fetchRequestHandler({ endpoint: '/api/trpc', req, router: appRouter, createContext, }); export { handler as GET, handler as POST };这是标准的 App Directory Route Handler 写法:将fetchRequestHandler同时挂到GET/POST上。源码注释中还留了一行// Add back once NextAuth v5 is released的runtime = 'edge'占位,说明边缘运行时支持当时受 NextAuth v5 状态限制,这也是一处值得注意的适用前提。
2.3 RSC 入口:nextCacheLink 直接调用,不走 HTTP
server-invoker.ts 是react-server条件下被解析的客户端:
export const api = experimental_createTRPCNextAppDirServer<typeof appRouter>({ config() { return { links: [ loggerLink({ enabled: (op) => true }), experimental_nextCacheLink({ // requests are cached for 5 seconds revalidate: 5, router: appRouter, transformer, createContext: async () => ({ session: await auth(), headers: { cookie: (await cookies()).toString(), 'x-trpc-source': 'rsc-invoke', }, }), }), ], }; }, });从源码结构看,该 link 有两个核心特征:
- 进程内直调:文件头注释写明 "This client invokes procedures directly on the server without fetching over HTTP",即传入
router后在 Node 进程中直接执行 procedure,省去 HTTP 往返; - Next.js 数据缓存接入:
revalidate: 5将结果按 Next.js 的revalidate语义缓存 5 秒,并复用请求级 context(session、cookie 头),同时通过x-trpc-source: rsc-invoke标记请求来源,便于服务端区分 RSC 直调与浏览器请求。
2.4 客户端入口:HTTP link + Server Action 双通道
client.ts 面向带'use client'的组件,暴露了两个对象:
export const api = experimental_createTRPCNextAppDirClient<AppRouter>({ config() { return { links: [ loggerLink({ enabled: (op) => true }), experimental_nextHttpLink({ transformer, batch: true, url: getUrl(), headers() { return { 'x-trpc-source': 'client' }; }, }), ], }; }, }); export const useAction = experimental_createActionHook<AppRouter>({ links: [loggerLink(), experimental_serverActionLink({ transformer })], });api通过experimental_nextHttpLink(batch: true)走/api/trpc端点,即 2.2 节的路由处理器;useAction是一个 React Hook,底层由experimental_serverActionLink承载,把 mutation 调用转换为 React Server Action 的表单提交语义。
两个入口共享同一份 shared.ts:getUrl()按环境返回端点地址(浏览器下为相对路径/api/trpc,服务端下回落到http://localhost:3000/api/trpc),transformer则是在superjson基础上注册了Temporal.PlainDate/Temporal.PlainDateTime自定义序列化器(依赖@js-temporal/polyfill),保证 Temporal 类型在跨边界传输后保真。
README 中引用的对照示例即 ClientGreeting.tsx(客户端侧)与 ServerInvokedGreeting.tsx(RSC 侧)。后者展示了 RSC 直调与失效按钮的完整形态:
export async function ServerInvokedGreeting() { const greeting1 = await api.greeting.query({ text: 'i never hit an api endpoint' }); const secret = await api.secret.query(); // ... <form action={async () => { 'use server'; await api.greeting.revalidate({ text: 'i never hit an api endpoint' }); }} > <button type="submit">Revalidate Cache 1</button> </form> }注意api.secret.query()的用法:直调发生在服务端进程内,可以访问仅服务端可达的数据(如会话密钥),这在经由 HTTP 的客户端调用中无法直接做到——从源码结构看,这是选择 RSC 直调而非一律走 API 端点的主要动机之一。
三、缓存与失效:revalidate procedure + 专用端点
缓存是 README 进度表中唯一"已完成"的能力项("Implement caching")。其落地由三部分组成:
nextCacheLink的revalidate参数(见 2.3 节),决定 RSC 查询结果在 Next.js data cache 中的存活时间;- router 中声明
revalidateprocedure:如上例api.greeting.revalidate(input),按 procedure 输入精确失效对应缓存项,调用入口是一个内联 Server Action('use server'); - HTTP 失效端点:src/app/api/trpc/revalidate/route.ts 只有一行——
export { experimental_revalidateEndpoint as POST } from '@trpc/next/app-dir/server';它把@trpc/next/app-dir/server导出的experimental_revalidateEndpoint直接挂载为POST处理器,供客户端(或浏览器)在不走完整 RPC 流程的情况下触发缓存失效。
需要强调边界:README 进度表中 "Implement cache invalidation on server calls / on client calls" 均为未勾选状态,意味着失效链路当前依赖显式的revalidate调用,尚不具备"一次 mutation 自动联动失效查询缓存"的能力。若你的业务强依赖自动失效,应把这一条列入风险清单。
四、Server Actions:createAction 把 procedure 变成 action
server-action 示例目录 给出了"procedure → Server Action"的标准包装方式:
'use server'; import { createAction, publicProcedure } from '~/server/trpc'; import { z } from 'zod'; export const testAction = createAction( publicProcedure .input(z.object({ text: z.string().min(1) })) .mutation(async (opts) => { console.log('testMutation called', opts); return { text: 'Hello world', date: new Date() }; }), );createAction复用 tRPC 的 procedure builder(输入校验、middleware 均可保留),仅在语义上把 mutation 标记为 Server Action 友好;客户端侧则通过 2.4 节的useActionHook 消费,目录中的ReactHookFormExample.action.tsx等文件进一步演示了与 React Hook Form 的表单集成(配合@hookform/resolvers与 zod)。
五、配套:TanStack Query 的 RSC 预取与水合
playground 中还内置了 RSC 预取链路,可直接作为"App Directory + TanStack Query + tRPC"的组合参考:
- 服务端 rq-server.tsx:
createTRPCOptionsProxy生成trpc选项代理(内部经server-only包锁定在服务端),并导出HydrateClient(基于dehydrate+HydrationBoundary)与prefetch(自动区分prefetchQuery/prefetchInfiniteQuery)。context 与 queryClient 均用cache()包裹,保证同一请求内稳定复用; - 客户端 rq-client.tsx:
getQueryClient采用"服务端每次新建、浏览器单例"的经典模式; - 序列化配置在 shared.ts:
staleTime设为 30 秒以避免水合后立即重复拉取,且shouldDehydrateQuery放开了pending状态查询的脱水——源码注释解释了原因:"we set a stale time so that queries aren't immediately refetched on the client" 以及 "include pending queries in dehydration ... allows us to prefetch in RSC and send promises over the RSC boundary",即允许把进行中的 Promise 经由 RSC 边界传给客户端续用; - 消费端示例见 rsc-rq-prefetch/post.tsx:
useSuspenseQuery(trpc.getLatestPost.queryOptions())搭配useMutation+invalidateQueries完成"读预取、写后手动失效"的闭环。
六、验证方式与工程约束
- 包脚本(package.json)提供
dev/build/start,以及基于 Playwright 的 e2e:test:e2e、test-dev(start-server-and-test起 3000 端口后跑playwright test); - 测试文件 test/client.test.ts 与 test/server-cache.test.ts 分别覆盖客户端行为与服务端缓存语义,是理解
nextCacheLink行为契约的入口; - 目录名
.experimental(带前导点)本身即仓库层面的"隔离"声明:README 提示若要贡献修改,应到上游仓库的对应目录进行,并通过官方社区渠道讨论方案取向(README 指向官方 Discord)。
七、适用判断:什么时候值得参考这个方案
综合 README 与源码,该实验方案的取舍可以归纳为:
- 收益:RSC 与客户端共享同一 router 类型与调用语法;RSC 直调省 HTTP 往返并可访问服务端私有数据;
revalidate机制让 RSC 数据缓存可控;procedure 体系(输入校验、中间件、transformer)完整保留。 - 代价与限制:全部 API 均带
experimental_前缀(experimental_createTRPCNextAppDirClient/experimental_nextHttpLink/experimental_createTRPCNextAppDirServer/experimental_nextCacheLink/experimental_serverActionLink/experimental_createActionHook/experimental_revalidateEndpoint),接口稳定性不保证;缓存自动失效链路未完成;README 明确警告生产使用可能伴随大规模重构。 - 建议姿势:把它当作"App Directory 下 tRPC 架构演进的活参考",重点研读
react-server入口拆分、nextCacheLink与 revalidate 端点的设计;若需在生产落地,当前更稳妥的组合仍是 README 提示的既有路径——组件内直接使用@trpc/client,或在客户端组件中沿用@trpc/next,并配合 next-minimal-starter 等稳定示例。
理解这一实验仓库,等于拿到了一张"tRPC 官方 App Directory 适配器"的需求清单与原型实现:入口按模块条件分流、RSC 进程内直调、缓存与失效显式化、Server Action 包装 procedure——这四点很可能就是正式适配器 API 定型时的骨架。
【免费下载链接】trpc🧙♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考