你还在用
useState
、useEffect
管理服务器状态?那你可能错过了前端开发的一场革命。
传统模式的噩梦:13行代码才能发个请求
先看看我们曾经是怎么折磨自己的:
tsx
// 传统方式:客户端状态管理的样板代码地狱
'use client'
export default function TodoList() {
const [todos, setTodos] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const addTodo = async (text) => {
setLoading(true)
setError(null)
try {
await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text })
})
// 还得手动重新获取数据
const response = await fetch('/api/todos')
setTodos(await response.json())
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
}
这 13 行样板代码 仅仅是为了发送一个 POST 请求。更要命的是,你还需要:
- 单独的 API 路由文件
- 手动的状态管理
- 手动的错误处理
- 手动的 loading 状态
- 手动的缓存失效
这就是传统的 客户端状态驱动模式,每一个数据变更都要走这套流程。
Server Actions:三行代码的反击
现在看看 Server Actions 是怎么碾压传统方式的:
tsx
// Server Actions 方式:服务器驱动的极简实现
async function addTodo(formData) {
'use server'
const text = formData.get('text')
await db.todo.create({ data: { text } })
revalidatePath('/todos')
}
// 使用:直接在表单中调用
export default async function TodoList() {
const todos = await db.todo.findMany()
return (
<form action={addTodo}>
<input name="text" />
<button type="submit">Add Todo</button>
</form>
)
}
3 行核心逻辑,没有状态管理,没有 API 路由,没有客户端 JavaScript。
这背后的魔法机制是什么?
编译时的魔法:RPC 思想的现代实现
Server Actions 的核心是一个古老而强大的概念:RPC (远程过程调用) 。你写的看似本地函数调用:
tsx
await addTodo(formData) // 看起来像本地调用
在编译时被 Next.js 自动转换成:
tsx
// 编译后的实际网络请求
await fetch('/__next_action_id__', {
method: 'POST',
body: formData,
headers: { 'Next-Action': 'action_id_hash' }
})
这就是为什么 tRPC、Telefunc 和 Zodios 的开发者会说:"Server Actions are presented as an implementation of an old RPC idea"。Next.js 把 RPC 的概念直接内建到了框架层面。
'use server'
指令告诉编译器:这个函数需要在服务器执行,请为它生成对应的网络端点和客户端代理。
缓存管理的五大武器
Server Actions 解决了状态同步,但数据缓存呢?Next.js 15 提供了完整的缓存控制工具包:
1. revalidatePath() - 精确制导
tsx
revalidatePath('/posts') // 失效特定页面
revalidatePath('/posts/[id]') // 失效动态路由
revalidatePath('/posts', 'layout') // 递归失效子路由
当你更新了帖子数据,只需要失效相关页面的缓存,其他页面继续享受缓存性能。
2. revalidateTag() - 范围打击
tsx
// 数据获取时打标签
const posts = await fetch('/api/posts', {
next: { tags: ['posts', 'user-content'] }
})
// 一次失效所有相关缓存
async function deleteUser() {
await db.user.delete({ where: { id } })
revalidateTag('user-content') // 失效用户相关的所有缓存
}
一个标签可以关联多个页面,一次失效,全部更新。
3. fetch() 的超能力
tsx
// 扩展版 fetch,内建缓存控制
fetch('/api/data', {
next: {
revalidate: 60, // 60秒后自动重新获取
tags: ['posts'] // 可被 revalidateTag 失效
}
})
4. unstable_cache() - 函数级缓存
tsx
const getCachedPosts = unstable_cache(
async () => db.posts.findMany(),
['posts-list'], // 缓存键
{
tags: ['posts'],
revalidate: 3600 // 1小时过期
}
)
把任何异步函数包装成可缓存版本。
5. unstable_noStore() - 强制动态
tsx
async function getRealTimeData() {
unstable_noStore() // 禁用缓存,每次都重新获取
return await db.getLatestStats()
}
三种重新验证策略的实战选择
Next.js 15 支持三种缓存重新验证方式,每种都有最适合的场景:
基于时间 (Time-based)
tsx
// 适合:相对稳定的数据,如文章列表
fetch('/api/posts', { next: { revalidate: 3600 } }) // 1小时更新一次
基于标签 (Tag-based)
tsx
// 适合:逻辑相关的多个缓存,如用户相关数据
revalidateTag('user-profile') // 失效用户档案相关的所有缓存
基于路径 (Path-based)
tsx
// 适合:页面级别的精确控制
revalidatePath('/dashboard') // 只更新仪表板页面
性能数据说话:传统 vs Server Actions
让数字说话,看看这场革命带来的实际收益:
传统模式的成本:
- 客户端 JavaScript 包大小:每个状态管理组件约 2-5KB
- 网络请求数:至少 2 次(页面加载 + API 调用)
- 开发维护成本:每个数据操作需要 10+ 行样板代码
Server Actions 的收益:
- 客户端 JavaScript:接近零(只有必要的表单处理)
- 网络请求:单次优化的 RSC 流
- 开发效率:每个数据操作只需 3-5 行核心逻辑
这不仅仅是代码量的减少,更是 开发心智模型 的简化。你不再需要考虑客户端状态同步,专注于业务逻辑本身。
架构演进:从客户端拉取到服务器推送
Server Actions 代表了前端架构的根本性转变:
传统模式:客户端驱动
用户操作 → 客户端状态变更 → API 调用 → 手动更新 UI
Server Actions:服务器驱动
用户操作 → 服务器执行 → 自动缓存失效 → RSC 推送更新
这种转变把复杂性从客户端转移到了构建时和服务器端,让运行时变得更加简单和可预测。
最后
Server Actions 不仅仅是一个新 API,它代表了前端开发的一个重要转折点。我们正在从 "客户端状态管理" 的复杂性中解脱出来,回归到更直接、更简单的 "服务器函数调用" 模式。
当你不再需要 useState
管理服务器数据,不再需要手写 API 路由,不再需要考虑客户端缓存同步时,你会发现自己有更多时间专注于真正重要的事情:构建用户真正需要的功能。
这就是 Server Actions 的真正价值:让简单的事情保持简单,让复杂的事情变得可管理。