Skip to content

Server Actions 全栈数据变更

Server Actions 是 Next.js 最革命性的特性之一——在服务端定义函数,在客户端直接调用,无需手动写 API 路由。它让数据变更(创建、更新、删除)变得极其简洁,配合表单、乐观更新和缓存重验证,构成了完整的全栈数据变更方案。

1. Server Actions 基础

1.1 什么是 Server Action

Server Action 是一个标记了 'use server' 的异步函数——它在服务端执行,但可以从客户端调用:

tsx
// app/chat/actions.ts
'use server'

import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'

export async function createChat(title: string) {
  const chat = await db.insert(chats).values({ title }).returning()
  revalidatePath('/chat')
  return chat[0]
}

底层原理:

客户端调用 createChat("新对话")


Next.js 自动创建一个 POST 请求到特殊端点


服务端执行 createChat 函数(直接访问数据库)


返回结果到客户端 + 触发缓存重验证

1.2 两种定义方式

tsx
// 方式1:独立文件(推荐 — 可复用)
// app/chat/actions.ts
'use server'

export async function createChat(title: string) { ... }
export async function deleteChat(id: string) { ... }

// 方式2:内联定义(简单场景)
// app/chat/page.tsx (Server Component)
export default function ChatPage() {
  async function handleCreate(formData: FormData) {
    'use server'
    const title = formData.get('title') as string
    await db.insert(chats).values({ title })
    revalidatePath('/chat')
  }

  return (
    <form action={handleCreate}>
      <input name="title" />
      <button type="submit">创建</button>
    </form>
  )
}

1.3 安全性

Server Actions 自动提供:

  • CSRF 保护:Next.js 自动添加和验证 CSRF Token
  • 代码不泄露:函数体永远不会发送到客户端
  • 输入加密:参数通过加密的 POST 请求传输

但你仍然需要做

  • 身份验证:每个 Server Action 都应检查用户身份
  • 授权检查:验证用户是否有权限执行该操作
  • 输入校验:使用 Zod 校验所有输入

2. 表单提交

2.1 基础表单

tsx
// Server Component — 渐进增强
export default function NewChatPage() {
  async function createChat(formData: FormData) {
    'use server'
    const title = formData.get('title') as string
    const model = formData.get('model') as string

    await db.insert(chats).values({ title, model })
    revalidatePath('/chat')
    redirect('/chat')
  }

  return (
    <form action={createChat}>
      <input name="title" placeholder="对话标题" required />
      <select name="model">
        <option value="gpt-4o">GPT-4o</option>
        <option value="claude-3.5-sonnet">Claude 3.5</option>
      </select>
      <button type="submit">创建</button>
    </form>
  )
}

渐进增强

<form action={serverAction}> 即使 JavaScript 被禁用也能工作——这就是渐进增强。浏览器会发送标准的表单 POST 请求。

2.2 带 Zod 校验的表单

tsx
// app/chat/actions.ts
'use server'

import { z } from 'zod'
import { auth } from '@/lib/auth/config'

const CreateChatSchema = z.object({
  title: z.string().min(1, '标题不能为空').max(100, '标题最多 100 字'),
  model: z.enum(['gpt-4o', 'claude-3.5-sonnet', 'gemini-pro']),
})

type FormState = {
  errors?: Record<string, string[]>
  message?: string
}

export async function createChat(
  prevState: FormState,
  formData: FormData,
): Promise<FormState> {
  // 1. 认证
  const session = await auth()
  if (!session?.user) {
    return { message: '请先登录' }
  }

  // 2. 校验
  const parsed = CreateChatSchema.safeParse({
    title: formData.get('title'),
    model: formData.get('model'),
  })

  if (!parsed.success) {
    return { errors: parsed.error.flatten().fieldErrors }
  }

  // 3. 执行
  const chat = await db.insert(chats).values({
    ...parsed.data,
    userId: session.user.id,
  }).returning()

  // 4. 重验证 + 重定向
  revalidatePath('/chat')
  redirect(`/chat/${chat[0].id}`)
}

2.3 useActionState 处理表单状态

tsx
// app/chat/new-chat-form.tsx
'use client'

import { useActionState } from 'react'
import { createChat } from './actions'

export function NewChatForm() {
  const [state, formAction, isPending] = useActionState(createChat, {})

  return (
    <form action={formAction}>
      <div>
        <input
          name="title"
          placeholder="对话标题"
          className={state.errors?.title ? 'border-red-500' : ''}
        />
        {state.errors?.title && (
          <p className="text-sm text-red-500">{state.errors.title[0]}</p>
        )}
      </div>

      <select name="model">
        <option value="gpt-4o">GPT-4o</option>
        <option value="claude-3.5-sonnet">Claude 3.5</option>
      </select>

      <button type="submit" disabled={isPending}>
        {isPending ? '创建中...' : '创建对话'}
      </button>

      {state.message && (
        <p className="text-sm text-red-500">{state.message}</p>
      )}
    </form>
  )
}

3. 乐观更新

3.1 useOptimistic

乐观更新让 UI 在 Server Action 完成之前就反映变化,提升感知速度:

tsx
'use client'

import { useOptimistic } from 'react'
import { toggleFavorite } from './actions'

type Chat = { id: string; title: string; isFavorite: boolean }

export function ChatList({ chats }: { chats: Chat[] }) {
  const [optimisticChats, addOptimistic] = useOptimistic(
    chats,
    (state, chatId: string) =>
      state.map((chat) =>
        chat.id === chatId
          ? { ...chat, isFavorite: !chat.isFavorite }
          : chat,
      ),
  )

  async function handleToggle(chatId: string) {
    addOptimistic(chatId) // 立即更新 UI
    await toggleFavorite(chatId) // 然后执行 Server Action
  }

  return (
    <ul>
      {optimisticChats.map((chat) => (
        <li key={chat.id}>
          {chat.title}
          <button onClick={() => handleToggle(chat.id)}>
            {chat.isFavorite ? '★' : '☆'}
          </button>
        </li>
      ))}
    </ul>
  )
}

用户体验:

用户点击收藏按钮


立即:星标变为已收藏(乐观更新)


后台:Server Action 执行数据库写入

    ├── 成功 → 保持当前状态
    └── 失败 → 自动回滚到原始状态

3.2 删除的乐观更新

tsx
'use client'

import { useOptimistic, useTransition } from 'react'
import { deleteChat } from './actions'

export function ChatList({ chats }: { chats: Chat[] }) {
  const [optimisticChats, removeOptimistic] = useOptimistic(
    chats,
    (state, deletedId: string) => state.filter((c) => c.id !== deletedId),
  )
  const [, startTransition] = useTransition()

  function handleDelete(chatId: string) {
    startTransition(async () => {
      removeOptimistic(chatId) // 立即从列表移除
      await deleteChat(chatId)
    })
  }

  return (
    <ul>
      {optimisticChats.map((chat) => (
        <li key={chat.id}>
          {chat.title}
          <button onClick={() => handleDelete(chat.id)}>删除</button>
        </li>
      ))}
    </ul>
  )
}

4. 缓存重验证

4.1 revalidatePath

tsx
'use server'

import { revalidatePath } from 'next/cache'

export async function createChat(title: string) {
  await db.insert(chats).values({ title })

  revalidatePath('/chat')          // 重验证 /chat 页面
  revalidatePath('/chat', 'layout') // 重验证 /chat 的 layout
  revalidatePath('/', 'layout')    // 重验证整个应用的 layout
}

4.2 revalidateTag

更精细的缓存控制——基于标签:

tsx
// 数据获取时打标签
async function getChats(userId: string) {
  return unstable_cache(
    () => db.query.chats.findMany({ where: eq(chats.userId, userId) }),
    [`chats-${userId}`],
    { tags: ['chats', `user-${userId}-chats`] },
  )()
}

// Server Action 中按标签重验证
'use server'
export async function createChat(title: string) {
  const session = await auth()
  await db.insert(chats).values({ title, userId: session.user.id })

  revalidateTag('chats')              // 所有用户的对话列表缓存都失效
  revalidateTag(`user-${session.user.id}-chats`) // 只失效当前用户的
}

4.3 redirect

tsx
'use server'

import { redirect } from 'next/navigation'

export async function createChat(formData: FormData) {
  const chat = await db.insert(chats).values({ ... }).returning()

  revalidatePath('/chat')
  redirect(`/chat/${chat[0].id}`) // 重定向到新对话页
  // redirect 之后的代码不会执行
}

redirect 必须在 try/catch 外面

redirect() 内部通过 throw 实现,如果放在 try/catch 中会被 catch 捕获:

tsx
// ❌ 错误
try {
  await db.insert(...)
  redirect('/chat') // 会被 catch 捕获
} catch (e) {
  // redirect 的 error 也会进入这里
}

// ✅ 正确
let chatId: string
try {
  const result = await db.insert(...).returning()
  chatId = result[0].id
} catch (e) {
  return { error: 'Failed' }
}
redirect(`/chat/${chatId}`)

5. 高级模式

5.1 非表单调用

Server Actions 不仅用于表单——任何 Client Component 都可以调用:

tsx
'use client'

export function DeleteButton({ chatId }: { chatId: string }) {
  return (
    <button
      onClick={async () => {
        const confirmed = window.confirm('确定删除?')
        if (confirmed) {
          await deleteChat(chatId)
        }
      }}
    >
      删除
    </button>
  )
}

5.2 文件上传

tsx
// actions.ts
'use server'

export async function uploadAvatar(formData: FormData) {
  const file = formData.get('avatar') as File
  if (!file || file.size === 0) return { error: '请选择文件' }

  const bytes = await file.arrayBuffer()
  const buffer = Buffer.from(bytes)

  // 上传到 S3 / R2
  const url = await uploadToStorage(buffer, file.name)

  await db.update(users).set({ avatarUrl: url }).where(eq(users.id, userId))
  revalidatePath('/settings')
}

5.3 Server Action 中间件模式

tsx
// lib/action-middleware.ts
import { auth } from '@/lib/auth/config'

export function withAuth<T extends (...args: any[]) => any>(action: T) {
  return async (...args: Parameters<T>) => {
    const session = await auth()
    if (!session?.user) {
      throw new Error('Unauthorized')
    }
    return action(...args)
  }
}

// 使用
'use server'
export const deleteChat = withAuth(async (chatId: string) => {
  await db.delete(chats).where(eq(chats.id, chatId))
  revalidatePath('/chat')
})

6. Server Actions vs Route Handlers

维度Server ActionsRoute Handlers
定义方式'use server' 函数route.ts 文件
调用方式直接调用函数HTTP 请求
类型安全端到端(参数+返回值)需要手动定义
表单集成<form action={}> 原生支持手动处理
流式响应
外部调用❌(只限应用内)✅(Webhook/第三方)
乐观更新useOptimistic 原生支持手动实现

本章小结

  • Server Actions 是标记 'use server' 的异步函数,服务端执行,客户端调用
  • 表单提交<form action={}> 支持渐进增强,useActionState 管理状态和 pending
  • Zod 校验:每个 Server Action 都必须校验输入、检查认证和授权
  • 乐观更新useOptimistic 让 UI 立即反映变化,失败自动回滚
  • 缓存重验证revalidatePath 按路径、revalidateTag 按标签精确失效
  • redirect 陷阱:不能放在 try/catch 中

下一章讲数据获取模式深度对比。

基于 MIT 协议开源