tRPC Next.js App Directory 实验适配器:RSC、Server Actions 与缓存的一体化实战指南
本篇围绕 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 节的 useAction Hook 消费,目录中的 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 定型时的骨架。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00