Server Actions 深度剖析:这就是个披着 React 外衣的 RPC

  • 本质: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 消除了:

  1. 客户端状态管理样板代码
  2. API 路由的中间层
  3. 手动缓存失效

本质是把数据变更从"客户端拉取"变成"服务器推送"。

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)
}

版本控制策略

  1. 功能标志:通过 feature flag 控制新旧版本
  2. 渐进迁移:组件级别的逐步升级
  3. 向后兼容:保持函数签名的向后兼容性

⚠️ 安全问题

所有标记 '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 专用 框架无关 框架无关

✅ 推荐做法

  1. 明确分工:内部交互用 Server Actions,对外 API 用 API Routes
  2. 安全第一:每个 Server Action 都要独立验证权限和输入
  3. 类型安全:充分利用 TypeScript 的端到端类型推导
  4. 代码组织:将相关的 Server Actions 组织在专门的文件中
  5. 渐进迁移:从新功能开始,逐步引入 Server Actions

❌ 避免的误区

  1. 全盘替换:不要试图用 Server Actions 替换所有 API Routes
  2. 忽略安全:不要假设 Server Actions 比 API Routes 更安全
  3. 过度复杂:不要在 Server Actions 中处理复杂的业务逻辑
  4. 类型忽视:不要忽略参数和返回值的类型定义

🔮 结论:架构革新的意义

Server Actions 不是对传统 Web 开发的颠覆,而是对 RPC 思想在 React 生态中的精妙实现。它的价值在于:

  • 编译时转换:将函数调用无缝转换为网络请求
  • RSC 集成:与 React Server Components 深度整合
  • 类型安全:提供端到端的 TypeScript 支持
  • 渐进增强:支持无 JavaScript 环境下的表单提交

技术意义

  • 降低了客户端-服务端交互的复杂度
  • 提供了更直观的全栈开发体验
  • 推动了 RPC 思想在现代 Web 开发中的回归
  • 为 React 生态的一体化发展奠定了基础
相关推荐
β添砖java3 小时前
案例二:登高千古第一绝句
前端·javascript·css
南雨北斗3 小时前
Vue 3 修饰符(Modifiers)
前端
会豪3 小时前
工业仿真(simulation)--前端(七)--消息栏
前端
Jinuss3 小时前
Vue3源码reactivity响应式篇之computed计算属性
前端·vue3
落日沉溺于海3 小时前
React From表单使用Formik和yup进行校验
开发语言·前端·javascript
知识分享小能手3 小时前
React学习教程,从入门到精通, React 新创建组件语法知识点及案例代码(11)
前端·javascript·学习·react.js·架构·前端框架·react
会豪3 小时前
工业仿真(simulation)--前端(五)--标尺,刻度尺
前端
会豪3 小时前
工业仿真(simulation)--前端(四)--画布编辑(2)
前端
an__ya__4 小时前
Vue数据响应式reactive
前端·javascript·vue.js