# 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 请求传输

::: warning 但你仍然需要做
- **身份验证**:每个 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>
  )
}
```

::: tip 渐进增强
`<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 之后的代码不会执行
}
```

::: warning 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 Actions | Route 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 中

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