各位好!我是你们的老朋友,今天咱们不聊虚的,咱们来聊聊全栈开发界的一对“绝代双骄”——tRPC 和 React Query。
我知道你们很多人听到“全栈类型安全”这五个字的时候,第一反应是:“别闹了,谁还维护两个文档?谁还写两个接口定义?”
没错,这正是今天讲座的主题。我们要消灭那种“前端写 TypeScript,后端写 any”的乱世。我们要构建一种架构,让后端的函数定义直接变成前端的组件,让 API 手册变成多余的废纸,甚至让“自描述”这个词,真正意义上变成代码的一部分。
准备好了吗?咱们开始。
第一章:如果你讨厌 API 手册,那你一定要听这个
先想象一下这样一个场景:
你的前端团队很棒,他们严格遵循 TypeScript 规范,定义了一个 User 接口:
// frontend/src/types/user.ts
interface User {
id: number;
username: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
然后你的后端团队也很棒,但后端在用 Java 或者 Python,或者仅仅是懒得同步类型。于是他们生成了一个 JSON Schema,或者随手写了一个 user.py 文件,里面写着:
# backend/models.py
class User:
id: str # 哦,是字符串,前端是 number
name: str
email: str
当你把这两个东西对接的时候,发生了什么?
前端开发者看着 id: number 的定义,兴高采烈地调用了接口。结果呢?运行时报错:Cannot read property 'toUpperCase' of undefined,或者更糟糕,TypeError: Cannot convert value of type 'string' to type 'number'。
这时候,你想起了那个已经被遗忘在服务器角落、落满灰尘的《API 接口文档 V1.0》。
“文档上说 id 是字符串!”前端吼道。
“那是旧版本!现在后台已经改成数字了!”后端吼道。
“你们这群没文化的!为什么不改回字符串?”
大家都很累,谁也没想加班,但代码挂了。
这就是没有类型安全的代价。我们每天写代码,90% 的时间都在处理这种“定义不一致”的垃圾时间。我们像是在玩大家来找茬,只不过找茬的对象是我们自己定义的接口。
tRPC 的出现,就是为了终结这种闹剧。
第二章:tRPC —— “不需要翻译的 RPC”
首先,纠正一个误区。tRPC 不是一种新的 RPC 协议(比如 gRPC 或 Thrift)。它不是让你去配置一堆 XML 文件或者 Protobuf 定义文件的。
tRPC 的核心哲学非常简单,甚至有点“霸道”:
如果你在服务器上定义了一个函数,那么在客户端,你应该能像调用本地函数一样调用它,并且拥有完全相同的类型定义。
它怎么做到的?这其实利用了 TypeScript 编译器的魔力。
当你定义了一个 Procedure(过程)时,tRPC 会捕获输入参数的类型和返回值的类型。然后,它会在运行时,通过 Proxies(代理)把这些类型“传送”给客户端。
简单来说,tRPC 的工作流程是这样的:
- 服务器端:你定义了
appRouter.procedure.input(z.object({ name: z.string() })).query(() => "Hello")。 - 编译时:TypeScript 编译器看到这个函数,确认了输入是
z.string(),输出是string。 - 客户端生成:tRPC 的 CLI 工具会读取服务器代码,自动生成客户端的代码。
- 客户端调用:你写
trpc.hello.query({ name: "Alice" })。这时候,TypeScript 就会介入,它发现你传了一个对象,并检查这个对象的属性name是否是string。如果传了age,编译器直接报错!这就是编译时错误!
看,这就是我们想要的。错误在代码写出来的那一刻就暴露了,而不是在用户点击“保存”按钮之后。
第三章:React Query —— tRPC 的最佳拍档
tRPC 解决了“类型”的问题,那“数据获取”呢?如果只是解决了类型,我们还得自己写 fetch 逻辑,自己处理 loading、error、retry,自己写缓存逻辑。那也太累了。
这时候,React Query(也就是大家熟悉的 TanStack Query)闪亮登场了。
React Query 的工作是管理服务端状态。它有一个核心理念:“缓存优先”。如果你之前请求过这个数据,它就不会再发起新的请求,除非你告诉它要更新。
tRPC 和 React Query 的结合,就像是给这辆车装上了涡轮增压。
tRPC 给你一个包含 data, isLoading, error 的对象(这很 React Query),而 React Query 负责在后台处理网络请求。
第四章:实战演练 —— 构建一个“全自动”的 TODO 应用
为了演示这个“无需 API 手册”的架构,我们来做一个极简的 TODO 应用。不需要数据库,我们就用内存模拟。
第一步:服务器端的“上帝视角”
在服务器端,你不需要写 JSON Schema,不需要写 Swagger 文档。你只需要写正常的 TypeScript 函数。
// server/src/router.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
// 初始化 tRPC
const t = initTRPC.create();
// 定义公共中间件:处理错误
const middleware = t.middleware(({ next }) => {
return next();
});
// 核心路由
export const appRouter = t.router({
// 这是一个查询 Procedure
// 我们定义了输入验证规则
getTodos: t.procedure
.input(
z.object({
limit: z.number().min(1).max(100).default(10), // 限制返回数量
status: z.enum(['pending', 'completed']).optional(), // 可选的枚举筛选
})
)
.query(async ({ input }) => {
// 模拟数据库查询
// 注意:这里不需要写文档说"limit 必须是 1-100"
return [
{ id: 1, title: '买咖啡', completed: false },
{ id: 2, title: '写代码', completed: true },
// 根据输入筛选...
];
}),
// 这是一个变更 Procedure
addTodo: t.procedure
.input(
z.object({
title: z.string().min(1).max(50), // 必须有标题,且长度 1-50
})
)
.mutation(async ({ input }) => {
// 模拟数据库插入
return { id: 3, title: input.title, completed: false };
}),
});
export type AppRouter = typeof appRouter;
看!这里定义了 limit 限制是 1-100,定义了 status 只能是 pending 或 completed。这些验证规则是硬编码在代码里的。
第二步:客户端的“零配置”接入
现在,神奇的事情发生了。在你的前端项目中,安装 @trpc/client 和 @trpc/react-query。
你不需要手动去写接口定义文件。你只需要创建一个 client 实例,然后从服务器导入你的 AppRouter 类型。
// client/src/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/src/router';
// 这个 hook 就是你未来的核心武器
export const trpc = createTRPCReact<AppRouter>();
第三步:在 React 组件中使用
现在,让我们来看看组件代码。没有任何 fetch 调用,没有任何 axios.get,甚至没有 URL。
// client/src/components/TodoList.tsx
import { trpc } from '../trpc';
import { useState } from 'react';
export const TodoList = () => {
const [input, setInput] = useState('');
// 1. 获取数据
// 你看这个 query 的参数,是不是看起来像调用本地函数?
// 而且,TypeScript 会自动推断出这里的 input.type 是什么。
const { data, isLoading, error } = trpc.getTodos.useQuery({
limit: 20,
status: 'pending', // 这里如果改成 'invalid_status',TypeScript 会直接报错!
});
// 2. 提交数据
const addTodoMutation = trpc.addTodo.useMutation();
if (isLoading) return <div>Loading... (Wait, why is it loading if it's local?)</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<ul>
{data?.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<form
onSubmit={(e) => {
e.preventDefault();
// 直接调用 mutation,不需要 axios.post
addTodoMutation.mutate({ title: input });
setInput('');
}}
>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
// 如果没输入,输入框就变灰,TypeScript 帮你做的
/>
<button disabled={!input || addTodoMutation.isLoading}>
{addTodoMutation.isLoading ? 'Adding...' : 'Add'}
</button>
</form>
</div>
);
};
这段代码的美妙之处在于:
- 类型推断:
trpc.getTodos.useQuery的返回值data,在 TypeScript 中直接就是Todo[]的类型。你不需要去写类型声明文件。 - 输入验证:我在代码里写了
limit: 20。如果我在组件里写了limit: 1000,TypeScript 编译器会直接报红,告诉你1000超过了max(100)的限制。这比任何运行时检查都更早地抓住了 bug。 - 错误处理:
error对象是强类型的。你不用去猜错误是 404 还是 500,TypeScript 会根据服务器返回的错误信息生成精确的错误类型。
这就是“自描述”。代码本身描述了它能做什么,输入什么,输出什么。代码即文档。
第五章:深入细节 —— Mutations、Infinite Queries 和 Server Components
光有一个简单的列表还不够。让我们再进阶一点,聊聊 Mutations 和 Infinite Queries。
1. Mutations(变更)的乐观 UI
当你要修改数据(比如更新头像、提交表单)时,React Query 的 useMutation 钩子非常强大。
假设服务器端有个更新用户的 Procedure:
// server
updateUser: t.procedure
.input(z.object({ id: z.number(), name: z.string() }))
.mutation(({ input }) => { /* ... */ })
客户端调用:
const updateMutation = trpc.updateUser.useMutation({
onSuccess: (data) => {
// 成功回调,TypeScript 保证 data 是 User 类型
console.log('Updated user:', data);
},
onError: (err) => {
// 错误回调,TypeScript 保证 err 是 TRPCError 类型
console.error('Update failed:', err);
}
});
最棒的是什么?是乐观更新。React Query 允许你在 mutation 开始的时候,立刻更新 UI,假设请求一定会成功。如果请求失败,它再自动回滚。这在交互体验上简直是降维打击。
2. 无限滚动
如果你要做一个无限滚动的 Feed 流(比如 Twitter 或 Hacker News),React Query 的 useInfiniteQuery 是标配。配合 tRPC,你可以轻松实现:
const { fetchNextPage, hasNextPage } = trpc.infiniteFeed.useInfiniteQuery(
{},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
// 这里你可以调用 tRPC procedure,传递分页参数
}
);
这里不再需要你去管 window.onscroll,也不需要自己去维护 page 状态。tRPC 和 React Query 自动帮你把参数映射好,自动帮你管理 nextCursor。
3. Next.js App Router 的完美结合
如果你使用 Next.js 13+ 的 App Router,tRPC 可以直接在 Server Components 中运行!
这意味着什么?意味着你可以直接在服务器端调用 tRPC Procedure!
// app/page.tsx (Server Component)
import { trpc } from '@/app/trpc'; // 客户端 client,但在服务器也能用
export default async function HomePage() {
// 在服务器端直接获取数据!
// 零客户端 JavaScript,极速首屏加载!
const todos = await trpc.getTodos.query({ limit: 10 });
return (
<div>
<h1>Server Side Rendered Data</h1>
<ul>
{todos.map(todo => <li key={todo.id}>{todo.title}</li>)}
</ul>
</div>
);
}
然后在客户端组件中:
// components/TodoInput.tsx (Client Component)
import { trpc } from '@/app/trpc';
export const TodoInput = () => {
// 在这里我们使用 React Query 的 useMutation
// React Query 会自动处理从 Server Component 获取的数据到 Client Component 的传递
const addTodo = trpc.addTodo.useMutation();
// ...
}
这种架构让 React Query 既能做 Server Components 的数据获取(通过 Server Actions 或者直接调用),又能做 Client Components 的交互(通过 useMutation)。
第六章:为什么说这是“无需 API 手册”?
现在,让我们来总结一下为什么这种架构能消灭 API 手册。
1. 类型即契约
传统的 API 手册(比如 Swagger UI)是一份静态的文档。开发人员可能看,也可能不看。文档更新了,但开发人员忘记同步更新代码了,这就是事故的源头。
在 tRPC 架构下,契约代码(Schema)是直接嵌入在项目源码里的。只要 Schema 不变,类型就不会变。你不需要找时间读文档,你只需要看类型定义。
2. 实时反馈
假设后端同事突然改了接口:status 字段从字符串改成了布尔值。
前端同事写代码时,IDE 会直接标红。如果他在调用时还传了字符串,浏览器会报错。
这种反馈是实时的,是零延迟的。这就是所谓的“上帝视角”。
3. 自动补全
当你调用 trpc.something.query() 时,你的 IDE 会自动列出所有可用的 Procedures。输入 query( 后,它会自动列出这个 Procedure 需要什么参数,参数有什么约束。这比任何文档都好用。
4. 验证即文档
在 tRPC 中,zod schema 不只是为了验证数据,它本身就是在告诉调用者:“嘿,这个参数是个字符串,而且必须大于 0”。这就是最好的文档。
第七章:幽默的“坑”与“药方”
当然,这门技术也不是完美的,虽然它很棒,但我们要诚实。
坑一:循环依赖
如果你在定义路由的时候,引入了另一个定义了路由的文件,或者引入了数据库模型,这可能会导致循环引用错误。
- 药方:不要在路由定义里引入业务逻辑代码。将业务逻辑抽离成纯函数,只保留类型定义在路由层。或者使用
defer。
坑二:复杂输入类型
如果你传的参数非常复杂,嵌套很深,客户端生成类型可能看着有点头疼。
- 药方:把复杂的输入拆分成多个小的 Procedure。比如不要写一个
createUser({ address: { city: ..., zip: ... }, profile: { ... } }),而是拆成createUser和updateProfile。
坑三:开发环境与生产环境的差异
tRPC 在开发环境会直接把类型传给前端。但在生产环境,它需要编译。如果你在 Server Component 里写了 await trpc.query(...),别忘了在构建命令里加上 tRPC 的生成脚本。
- 药方:配置好
tsc-alias和@trpc/server的编译配置。不要相信“魔法”永远生效,你要知道魔法背后的原理。
第八章:最后的话 —— 开发者的自由
各位,写代码的最高境界是什么?是消除不确定性。
以前,你调用一个接口,你不知道它会不会挂,不知道参数对不对,不知道返回值是什么。
现在,有了 tRPC 和 React Query 的深度融合,你的代码里充满了确定性。每一行代码,每一个变量,都受 Type Safety 的保护。
React Query 解决了“状态管理”的痛苦,tRPC 解决了“类型同步”的痛苦。两者的结合,就像是给你的全栈应用装上了一副防弹盔甲和一副导航眼镜。
在这个架构下,前端开发者不再需要跟后端吵吵嚷嚷地确认参数格式,不再需要维护两套代码。后端开发者也不需要费劲写 Swagger 文档。
代码即文档,编译即测试。
这不仅仅是一个技术栈的选择,更是一种工作流的革命。它让我们从繁琐的样板代码中解放出来,去思考更有趣的业务逻辑,去构建更流畅的用户体验。
所以,别再纠结你的 API 手册有没有更新了,把你的时间花在写代码上吧。毕竟,写代码比维护文档有趣多了,不是吗?
好了,今天的讲座就到这里。希望大家回去以后,能把那些该死的 any 全部干掉,拥抱类型,拥抱 tRPC,拥抱 React Query!
谢谢大家!