- 本质:Server Actions 是专为 React 生态设计的 RPC 实现
- 机制:编译时转换 + 运行时函数映射 + RSC Payload 集成
- 定位:不是 API Routes 的替代品,而是互补的高层抽象
- 价值:简化客户端-服务端交互,提供端到端类型安全
🔍 Server Actions?
Server Actions 本质上是 Remote Procedure Call (RPC) 在 React Server Components 生态中的实现。正如 Next.js 官方所说:
"Server Actions are presented as an implementation of an old RPC idea explored in TRPC, Telefunc and Zodios."
它让开发者能够像调用本地函数一样调用服务器端函数,抽象掉 HTTP 请求的复杂性。
基本语法
tsx
// Server Action - 服务器端函数
export async function createPost(formData: FormData) {
'use server' // 编译时指令
const title = formData.get('title')
const post = await db.posts.create({ title })
revalidatePath('/posts') // 自动缓存失效
return post
}
// 客户端调用 - 看起来像本地函数
const handleSubmit = async (formData) => {
await createPost(formData) // 实际发送网络请求
}
编译时转换
1. 'use server'
指令处理
当 Next.js 编译器遇到 'use server'
指令时:
tsx
// 源代码
export async function createUser(formData) {
'use server'
return await db.users.create(...)
}
// 编译后的客户端代码
export const createUser = function(formData) {
return __invokeServerAction('action_id_abc123', [formData])
}
2. 唯一标识符生成
每个 Server Action 获得一个不可预测的唯一 ID:
tsx
// Next.js 内部注册机制(简化版)
const serverActions = new Map([
['action_id_abc123', {
module: '/app/actions.js',
export: 'createUser',
function: createUser
}]
])
流程
网络传输层
Server Actions 强制使用 POST 请求,请求发送到当前页面路由:
tsx
POST /current-page-route
Content-Type: text/plain;charset=UTF-8
Next-Action: action_id_abc123
Next-Router-State-Tree: [路由状态树]
[序列化的参数数据]
服务器端处理
tsx
// 服务器收到请求后的处理流程
function handleServerAction(request) {
const actionId = request.headers.get('Next-Action')
const action = serverActions.get(actionId)
if (!action) throw new Error('Action not found')
const params = deserializeParams(request.body)
const result = await action.function(...params)
// 生成 RSC Payload(包含更新的 UI 状态)
return generateRSCPayload(result)
}
🆚 API Routes
技术哲学差异
维度 | Server Actions | API Routes |
---|---|---|
抽象层级 | 高层抽象(函数调用) | 底层抽象(HTTP 端点) |
调用方式 | await createPost(data) |
fetch('/api/posts', {...}) |
HTTP 方法 | 仅 POST | GET/POST/PUT/DELETE/等 |
类型安全 | 端到端 TypeScript | 需手动类型定义 |
错误处理 | JavaScript 异常 | HTTP 状态码 |
缓存控制 | revalidatePath/Tag |
手动响应头 |
表单集成 | 原生 <form action={}> |
手动 onSubmit + fetch |
渐进增强 | 无 JavaScript 也可用 | 依赖 JavaScript |
场景
✅ Server Actions
- 表单提交和数据变更
- 组件内的服务器逻辑
- 需要渐进增强的交互
- 简单的 CRUD 操作
javascript
// 典型用例:用户资料更新
export async function updateProfile(formData: FormData) {
'use server'
const session = await auth()
if (!session) throw new Error('Unauthorized')
const data = Object.fromEntries(formData)
await db.user.update({
where: { id: session.user.id },
data
})
revalidatePath('/profile')
}
✅ API Routes
- 对外公共 API
- 第三方系统集成
- 复杂 HTTP 语义需求
- 微服务架构接入层
javascript
// 典型用例:公共 API 端点
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const posts = await db.posts.findMany({
skip: (page - 1) * 10,
take: 10
})
return Response.json(posts, {
headers: {
'Cache-Control': 's-maxage=300',
'X-Total-Count': posts.length.toString()
}
})
}
🔗 数据流重构
传统模式
arduino
Server Component → fetch API Route → 客户端状态更新 → UI 重渲染
Server Actions 模式
arduino
Server Component → Server Action → 自动缓存失效 → RSC Payload → UI 自动更新
api route
tsx// API 路由: /api/todos export async function POST(request) { const data = await request.json() await db.todo.create({ data }) return Response.json({ success: true }) } // 客户端组件 'use client' export default function TodoList() { const [todos, setTodos] = useState([]) const [loading, setLoading] = useState(false) const addTodo = async (text) => { setLoading(true) await fetch('/api/todos', { method: 'POST', body: JSON.stringify({ text }) }) // 手动重新获取数据 const response = await fetch('/api/todos') setTodos(await response.json()) setLoading(false) } return <TodoForm onSubmit={addTodo} /> }
Server Actions
tsx// Server Action async function addTodo(text) { 'use server' await db.todo.create({ data: { text } }) revalidatePath('/todos') // 自动失效缓存 } // Server Component export default async function TodoList() { const todos = await db.todo.findMany() return ( <form action={addTodo}> <input name="text" /> <button type="submit">Add</button> </form> ) }
区别:
传统模式:客户端驱动
- 需要手动管理状态、loading、错误
- 多次网络请求(页面 + API)
- 客户端 JS 包大
Server Actions:服务器驱动
- 框架自动处理状态同步
- 单次优化的 RSC 流
- 零客户端 JS
Server Actions 消除了:
- 客户端状态管理样板代码
- API 路由的中间层
- 手动缓存失效
本质是把数据变更从"客户端拉取"变成"服务器推送"。
RSC Payload 的威力
javascript
// Server Component:数据获取
export default async function PostList() {
const posts = await getPosts()
return (
<div>
{posts.map(post => (
<PostItem
key={post.id}
post={post}
deleteAction={deletePost} // 传递 Server Action
/>
))}
</div>
)
}
// Client Component:用户交互
'use client'
export function PostItem({ post, deleteAction }) {
return (
<div>
<h3>{post.title}</h3>
<form action={deleteAction}>
<input type="hidden" name="id" value={post.id} />
<button type="submit">删除</button>
</form>
</div>
)
}
// Server Action:数据变更
async function deletePost(formData: FormData) {
'use server'
const id = formData.get('id')
await db.posts.delete({ where: { id } })
revalidatePath('/posts') // 触发 PostList 重新渲染
}
当 deletePost
执行后,Next.js 自动返回更新的 RSC Payload,客户端无需手动管理状态即可看到最新的帖子列表。
🔄 API 版本化
传统 API Routes 版本化
javascript
// URL 路径版本化
/api/v1/posts/route.js → GET /api/v1/posts
/api/v2/posts/route.js → GET /api/v2/posts
Server Actions 版本化
由于 Server Actions 与组件紧耦合,版本化策略需要重新思考:
javascript
// 函数签名演进(向后兼容)
// v1
export async function createPost(title: string) {
'use server'
return await db.posts.create({ title })
}
// v2:增加可选参数
export async function createPost(title: string, content?: string) {
'use server'
return await db.posts.create({ title, content })
}
// v3:重构为对象参数
export async function createPost(data: { title: string; content?: string; tags?: string[] }) {
'use server'
return await db.posts.create(data)
}
版本控制策略:
- 功能标志:通过 feature flag 控制新旧版本
- 渐进迁移:组件级别的逐步升级
- 向后兼容:保持函数签名的向后兼容性
⚠️ 安全问题
所有标记 'use server'
的函数都会成为公开的 HTTP 端点,即使没有被导出:
javascript
// ❌ 危险:这会成为可访问的端点
async function dangerousAdminFunction() {
'use server'
// 攻击者可以直接调用!
return await db.admin.deleteAllUsers()
}
1. 权限验证
javascript
export async function deleteUser(userId: string) {
'use server'
// ✅ 每个 action 都要独立验证权限
const session = await auth()
if (!session?.user?.isAdmin) {
throw new Error('Unauthorized')
}
return await db.users.delete({ where: { id: userId } })
}
2. 输入验证
javascript
import { z } from 'zod'
const CreatePostSchema = z.object({
title: z.string().min(1).max(100),
content: z.string().max(5000)
})
export async function createPost(formData: FormData) {
'use server'
// ✅ 严格的输入验证
const rawData = Object.fromEntries(formData)
const validatedData = CreatePostSchema.parse(rawData)
return await db.posts.create({ data: validatedData })
}
3. 代码组织
javascript
// ✅ 推荐:专门的 actions 文件
'use server' // 文件级指令
// 所有导出的函数都是 Server Actions
export async function createPost(data: CreatePostData) { ... }
export async function updatePost(id: string, data: UpdatePostData) { ... }
export async function deletePost(id: string) { ... }
📊 性能
Bundle 大小
- Server Actions:每个 action ~200-500 bytes(序列化逻辑)
- API Routes:每个调用 ~100-200 bytes + 错误处理代码
网络请求
javascript
// 传统方式:多次网络往返
const response1 = await fetch('/api/validate', {...})
if (response1.ok) {
const response2 = await fetch('/api/create', {...})
if (response2.ok) {
// 手动更新 UI 状态
setData(await response2.json())
}
}
// Server Actions:单次调用 + 自动 UI 更新
await createPostWithValidation(formData)
// Next.js 自动返回更新的 RSC Payload,UI 自动更新
与其他 RPC 方案对比
特性 | Next.js Server Actions | tRPC | GraphQL |
---|---|---|---|
调用语法 | await createPost(data) |
api.post.create.mutate(data) |
mutate({ variables: data }) |
类型安全 | TypeScript 原生 | 端到端推导 | 代码生成 |
缓存策略 | RSC Payload | TanStack Query | Apollo/Relay |
学习成本 | 低(基于函数) | 中等 | 高(新语言) |
生态集成 | React/Next.js 专用 | 框架无关 | 框架无关 |
✅ 推荐做法
- 明确分工:内部交互用 Server Actions,对外 API 用 API Routes
- 安全第一:每个 Server Action 都要独立验证权限和输入
- 类型安全:充分利用 TypeScript 的端到端类型推导
- 代码组织:将相关的 Server Actions 组织在专门的文件中
- 渐进迁移:从新功能开始,逐步引入 Server Actions
❌ 避免的误区
- 全盘替换:不要试图用 Server Actions 替换所有 API Routes
- 忽略安全:不要假设 Server Actions 比 API Routes 更安全
- 过度复杂:不要在 Server Actions 中处理复杂的业务逻辑
- 类型忽视:不要忽略参数和返回值的类型定义
🔮 结论:架构革新的意义
Server Actions 不是对传统 Web 开发的颠覆,而是对 RPC 思想在 React 生态中的精妙实现。它的价值在于:
- 编译时转换:将函数调用无缝转换为网络请求
- RSC 集成:与 React Server Components 深度整合
- 类型安全:提供端到端的 TypeScript 支持
- 渐进增强:支持无 JavaScript 环境下的表单提交
技术意义
- 降低了客户端-服务端交互的复杂度
- 提供了更直观的全栈开发体验
- 推动了 RPC 思想在现代 Web 开发中的回归
- 为 React 生态的一体化发展奠定了基础