Server Actions 完整指南
Server Actions 是 Next.js(App Router 体系)里最有“范式转移”意味的能力之一:它把“从浏览器提交数据 → 走 API → 服务端写库 → 客户端再拉取数据”这条链路,压缩成在 UI 里直接调用一个服务端函数。
但它并不是“把后端写进前端”,而是一套围绕 RSC(React Server Components)边界、请求序列化、缓存与失效、安全隔离 重新定义的交互协议。
这篇文章按“能在生产落地”为标准,依次讲清:
- 什么时候该用/不该用 Server Actions
'use server'的语义与编译期边界- 表单提交、并发与幂等
- 权限、校验、CSRF/重放、敏感信息
- 缓存与 revalidate 的正确打开方式
- 一套可复用的工程化结构(Action 层、schema、错误码)
1. Server Actions 解决的到底是什么问题?
在传统前端架构里,“写操作”通常需要两套代码:
- UI 层构造请求(fetch/axios)
- API 层解析请求、校验、执行业务
这导致三个常见痛点:
- 样板代码多:DTO、路由、controller、错误映射
- 类型不一致:前后端 schema 漂移,靠约定或手写 types
- 缓存/失效复杂:写完数据后,页面该怎么更新?哪些缓存该失效?
Server Actions 的核心价值是:
- 把“写操作”收敛成一个服务端函数,由框架负责把调用从 client 路由到 server;
- 在 App Router 的缓存语义下,提供与页面缓存/请求缓存更一致的“失效”手段。
你可以把它理解为:Next.js 给“写操作”提供了一个与 RSC/缓存模型原生兼容的调用入口。
2. 心智模型:Server Actions 与 RSC 的边界
2.1 'use server' 不是运行时开关,而是编译期边界
在 Next.js 中:
'use client'/'use server'是模块级/函数级的编译指令。- 被标记为 Server Action 的函数只能在服务端执行;在客户端调用时,框架会生成一个“可调用的引用”,最终由服务端执行。
一个直观的理解:
- 你写的是函数;
- 框架生成的是 RPC(带序列化、鉴权/上下文、执行、返回)。
2.2 传参限制:可序列化是第一原则
Server Action 的参数需要能安全序列化。
建议遵循:
- 只传 primitive / plain object / array / FormData
- 不传 class 实例、函数、DOM 节点、复杂原型对象
如果你发现自己想把“整个业务对象”丢进去,通常意味着你应该传一个 id 或 payload,然后在服务端再查询/校验。
3. 两种主流用法:表单 Action 与事件 Action
3.1 表单提交(推荐):天然 CSRF 友好,契合 Web 标准
Server Actions 最推荐的入口是表单:
// app/settings/page.tsx
import { updateProfileAction } from './actions'
export default function SettingsPage() {
return (
<form action={updateProfileAction}>
<input name="displayName" />
<button type="submit">保存</button>
</form>
)
}
在服务端:
// app/settings/actions.ts
'use server'
export async function updateProfileAction(formData: FormData) {
const displayName = String(formData.get('displayName') || '').trim()
// 校验、鉴权、写库...
}
为什么推荐表单?
- 浏览器原生语义:回退/刷新更自然
- 你更容易把“输入 → 校验 → 保存”做成明确的流水线
- 比“按钮 onClick 调 action”更不容易写出隐式并发 bug
3.2 事件触发(慎用):更像 RPC,必须处理并发与幂等
'use client'
import { toggleStarAction } from './actions'
export function StarButton({ id }: { id: string }) {
return (
<button
onClick={async () => {
await toggleStarAction(id)
}}
>
Star
</button>
)
}
这类用法更像“客户端发起 RPC”,你需要格外注意:
- 重复点击导致并发
- 网络抖动导致重试
- 乐观更新与最终一致
4. 工程化落地:Action 层应该长什么样?
一个能长期维护的结构,通常把“Action 的对外接口”与“业务实现”分离:
actions/*.ts:只负责输入解析、校验、鉴权、错误映射、触发失效services/*.ts:纯业务逻辑(可被 API/任务队列复用)schemas/*.ts:输入 schema(建议用 zod 或你们统一的校验工具)
示例:
// app/settings/schemas.ts
import { z } from 'zod'
export const updateProfileSchema = z.object({
displayName: z.string().min(2).max(32),
})
// app/settings/services.ts
export async function updateProfile(userId: string, input: { displayName: string }) {
// 写库、写缓存、发事件...
}
// app/settings/actions.ts
'use server'
import { updateProfileSchema } from './schemas'
import { updateProfile } from './services'
import { revalidatePath } from 'next/cache'
class ActionError extends Error {
code: string
constructor(code: string, message: string) {
super(message)
this.code = code
}
}
function requireUserId(): string {
// 伪代码:从会话中拿 user
// const user = await auth()
// if (!user) throw new ActionError('UNAUTHORIZED', '请先登录')
return 'user_123'
}
export async function updateProfileAction(formData: FormData) {
const userId = requireUserId()
const input = {
displayName: String(formData.get('displayName') || '').trim(),
}
const parsed = updateProfileSchema.safeParse(input)
if (!parsed.success) {
throw new ActionError('VALIDATION_ERROR', '输入不合法')
}
await updateProfile(userId, parsed.data)
// 关键:写操作后做缓存失效
revalidatePath('/settings')
}
这个结构的好处:
- Action 层是“协议适配器”,服务层是“领域逻辑”
- 输入校验与鉴权在入口处统一完成
- 缓存失效在入口处统一声明
5. 安全边界:你必须明确回答的 5 个问题
Server Actions 容易让人产生错觉:“既然在 server 执行,那就是安全的”。
更准确的说法是:Server Actions 让调用路径更短,但安全问题一个都不会消失。
5.1 认证:Action 里必须做鉴权
不要假设“只有页面能调用它”。
- 攻击者可以构造请求直接触发 action
- 也可能通过 XSS / 依赖污染从客户端触发
结论:每个会修改数据的 action,都要在服务端检查身份与权限。
5.2 输入校验:永远不要信任 FormData
- 字段缺失、空字符串、超长、类型错
- 业务约束(例如“昵称不得包含敏感词”)
建议:用 schema 校验(zod)或你们统一的 validator。
5.3 幂等与重放:写操作要能承受重复执行
浏览器/网络层可能导致重复提交。
常见策略:
- 业务幂等键:
idempotencyKey - 数据层唯一约束:例如
(userId, itemId)唯一 - 事务:确保写入一致
5.4 CSRF:优先走 form action,并启用 same-site 策略
Server Actions 不等于“自动免疫 CSRF”。
- 如果你依赖 cookie 会话,仍要考虑跨站请求
- form action 的默认行为更符合浏览器标准,但你仍需要正确配置 cookie 的
SameSite和 token
5.5 错误回传:不要把内部错误直接暴露给用户
Action 抛出的错误最终会影响 UI。
建议:
- 对用户展示:可理解的、稳定的错误码/文案
- 对服务端记录:完整 stack、请求上下文、userId、traceId
6. 缓存语义:写操作之后,页面为什么不更新?
在 App Router 模型里,“看起来像 SSR 的页面”实际上可能被缓存。
你需要掌握 3 个概念:
- 请求缓存(fetch cache)
- 页面/路由段缓存(RSC payload cache)
- 失效机制(revalidatePath / revalidateTag)
6.1 写操作后,推荐怎么做?
- 更新某个页面:
revalidatePath('/xxx') - 更新多个数据源:给 fetch 打 tag,然后
revalidateTag('tag-name')
建议的经验法则:
- 页面级别更新不多:优先
revalidatePath - 数据在多个页面复用:优先
revalidateTag
6.2 不要滥用 revalidatePath('/')
这会让你的缓存命中率断崖式下降。
更好的做法:
- 只失效受影响的 route segment
- 用 tag 精准失效
7. 并发与用户体验:让 Action 看起来“快而可靠”
7.1 useTransition 与 Pending 状态
当 action 触发后,用户需要看到明确反馈。
- 按钮 loading
- 禁用重复提交
- 成功/失败 toast
7.2 乐观更新(谨慎)
乐观更新适合:
- 可快速回滚
- 失败概率低
- 用户对即时反馈敏感(点赞/收藏)
对“资金/权限/关键数据”写操作不建议乐观更新。
8. 什么时候不该用 Server Actions?
- 需要对外开放的公共 API(给第三方/移动端)
- 需要长时间运行的任务(应改为队列/异步任务)
- 需要复杂流控/网关策略(限流、WAF、跨服务认证)
Server Actions 是“Web 应用内的写操作入口”,不是 API 网关替代品。
9. 生产级检查清单
在把某个写操作迁移到 Server Actions 前,按这份清单过一遍:
- action 是否做了鉴权与权限校验
- 输入是否做了 schema 校验
- 是否具备幂等策略(或数据层唯一约束)
- 是否定义了稳定的错误码/文案
- 写操作后是否做了精确的缓存失效(path/tag)
- 是否有可观测性:日志、traceId、错误上报
10. 总结
Server Actions 的正确价值不是“少写 API”,而是:
- 把写操作纳入 App Router/RSC 的统一模型
- 让缓存失效与 UI 刷新更一致
- 在正确工程结构下,显著减少样板代码与类型漂移


