Appearance
第13章 Server Actions 与数据流
本章要点
- 从 API Routes 到 Server Actions:服务端调用范式的三次跃迁
- "use server" 指令的编译时处理:如何将函数调用转化为网络请求
- Action ID 的生成算法:哈希、模块路径与函数位置的三元组
- FormData 序列化与渐进增强:没有 JavaScript 的表单如何工作
- 乐观更新与错误处理的统一模型:useOptimistic 与 useActionState 的协作
- CSRF 防护与闭包变量泄露:Server Actions 的安全攻击面分析
- Server Actions 与 React Server Components 的数据流闭环
在传统的 Web 开发中,前端与后端之间始终存在一道鸿沟——HTTP 协议。无论你使用 REST、GraphQL 还是 tRPC,开发者都必须手动定义请求格式、维护 API 端点、处理序列化与反序列化。这些"胶水代码"在一个全栈应用中往往占据了惊人的比例。React Server Actions 的出现,正是为了消除这道鸿沟。
Server Actions 让你可以在客户端组件中直接调用服务端函数,就像调用一个普通的异步函数一样。编译器负责将这个"函数调用"转化为一个 HTTP 请求,将参数序列化为请求体,将返回值反序列化为客户端可用的数据。这听起来像是 RPC(远程过程调用)的老概念,但 React 的实现远比传统 RPC 深刻——它将服务端调用与表单提交、乐观更新、错误边界、并发渲染等 React 核心机制深度融合,形成了一套完整的数据变更(mutation)基础设施。
本章将从编译时到运行时,完整剖析 Server Actions 的内部机制。我们会看到"use server"这两个字背后的编译器魔法,理解 Action ID 的生成策略,分析 FormData 序列化的工程细节,深入乐观更新的双层状态模型,最后严肃审视 Server Actions 引入的安全风险。
13.1 从 API Routes 到 Server Actions:服务端调用范式的进化
13.1.1 三代服务端调用范式
让我们用一个简单的"创建待办事项"场景,回顾服务端调用范式的三次进化:
第一代:手动 fetch + API Routes
typescript
// pages/api/todos.ts(服务端)
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
const { title } = req.body;
const todo = await db.todo.create({ data: { title } });
res.status(201).json(todo);
}
}
// components/TodoForm.tsx(客户端)
function TodoForm() {
const [title, setTitle] = useState('');
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsPending(true);
setError(null);
try {
const res = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title }),
});
if (!res.ok) throw new Error('Failed to create todo');
const todo = await res.json();
// 还需要手动更新 UI...
router.refresh();
} catch (e) {
setError(e.message);
} finally {
setIsPending(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input value={title} onChange={e => setTitle(e.target.value)} />
<button disabled={isPending}>
{isPending ? 'Adding...' : 'Add'}
</button>
{error && <p className="error">{error}</p>}
</form>
);
}数一数这段代码中的"胶水":API 路由定义、HTTP 方法判断、请求头设置、JSON 序列化/反序列化、手动 pending 状态管理、手动错误处理、手动 UI 刷新。真正的业务逻辑只有一行:db.todo.create({ data: { title } })。
第二代:tRPC / React Query
typescript
// server/routers/todo.ts
export const todoRouter = router({
create: publicProcedure
.input(z.object({ title: z.string() }))
.mutation(async ({ input }) => {
return db.todo.create({ data: { title: input.title } });
}),
});
// components/TodoForm.tsx
function TodoForm() {
const utils = trpc.useUtils();
const mutation = trpc.todo.create.useMutation({
onSuccess: () => utils.todo.list.invalidate(),
});
return (
<form onSubmit={e => {
e.preventDefault();
mutation.mutate({ title: e.currentTarget.title.value });
}}>
<input name="title" />
<button disabled={mutation.isPending}>
{mutation.isPending ? 'Adding...' : 'Add'}
</button>
{mutation.error && <p className="error">{mutation.error.message}</p>}
</form>
);
}tRPC 消除了 HTTP 层的样板代码,提供了端到端的类型安全。但它仍然是一个独立于 React 的解决方案——pending 状态、错误处理、缓存失效都由第三方库管理,与 React 的并发渲染和 Suspense 是"贴合"而非"融合"。
第三代:Server Actions
typescript
// app/actions.ts
'use server';
export async function createTodo(formData: FormData) {
const title = formData.get('title') as string;
const todo = await db.todo.create({ data: { title } });
revalidatePath('/todos');
return todo;
}
// components/TodoForm.tsx
import { createTodo } from '@/app/actions';
function TodoForm() {
const [state, formAction, isPending] = useActionState(createTodo, null);
return (
<form action={formAction}>
<input name="title" />
<button disabled={isPending}>
{isPending ? 'Adding...' : 'Add'}
</button>
</form>
);
}Server Actions 带来的简化是质的飞跃:没有 API 路由、没有 fetch、没有手动序列化、pending 状态由 React 内建追踪、表单即使在 JavaScript 未加载时也能通过原生 HTML 表单提交工作。更重要的是,这不是一个独立的数据层方案,而是 React 渲染引擎的一部分。
13.1.2 Server Actions 的本质:RPC 的 React 化
Server Actions 的设计灵感来自远程过程调用(RPC),但它超越了传统 RPC 的范畴。传统 RPC 框架关注的是"如何让远程调用看起来像本地调用",而 Server Actions 关注的是"如何让服务端数据变更与 React 的渲染模型无缝集成"。
传统 RPC: Client Function Call → Network → Server Function Execution → Return Value
Server Actions: Client Form/Action → Transition → Network → Server Function → RSC Re-render → Streaming UI Update下图展示了 Server Actions 的完整调用链路,从客户端触发到 UI 更新的闭环:
关键区别在于中间的"Transition"和末尾的"RSC Re-render"。Server Actions 的调用被包装在 React 的 Transition 中,这意味着:
- 调用期间,旧 UI 保持交互性——不会出现空白或 loading 闪烁
- 返回的不仅是数据,而是重新渲染后的 RSC 流——服务端组件树自动更新
- 乐观更新、错误回退、并发调用都由 React 统一调度——开发者无需自建状态机
深度洞察:Server Actions 最深刻的创新不在于"消除 API 路由",而在于将数据变更(mutation)纳入了 React 的声明式范式。在 Server Actions 之前,React 是一个优秀的"读取框架"——从 state 到 UI 的映射是声明式的。但数据的写入、提交、乐观更新却是命令式的。Server Actions 让数据写入也变成了声明式的:你声明一个 action,React 负责执行时机、状态追踪、错误恢复和 UI 更新。
13.2 "use server" 指令的编译时处理
13.2.1 指令的语义
"use server" 是 React 引入的第二个指令(第一个是 "use client")。它有两种使用方式:
typescript
// 方式 1:模块级指令——整个文件中导出的所有函数都是 Server Actions
'use server';
export async function createTodo(formData: FormData) {
// 这是一个 Server Action
}
export async function deleteTodo(id: string) {
// 这也是一个 Server Action
}
// 方式 2:函数级指令——单个函数声明为 Server Action
export function TodoList() {
async function handleDelete(id: string) {
'use server';
// 这个内联函数是一个 Server Action
await db.todo.delete({ where: { id } });
revalidatePath('/todos');
}
return <DeleteButton onDelete={handleDelete} />;
}