服务器操作Skill server-actions

Next.js Server Actions 是一种技术,允许在Next.js应用中定义服务器端函数来处理表单提交、数据变异等操作,无需编写API路由。它简化了前端与后端的交互,提高了开发效率和用户体验。关键词:Next.js、服务器操作、表单处理、数据变异、useFormState、useFormStatus、revalidatePath、revalidateTag。

前端开发 0 次安装 0 次浏览 更新于 3/8/2026

name: server-actions description: 当用户询问关于"Server Actions"、“Next.js中的表单处理”、“mutations”、“useFormState”、“useFormStatus”、“revalidatePath”、“revalidateTag”,或需要关于Next.js App Router中数据变异和表单提交的指导时,应使用此技能。 version: 1.0.0

Next.js 服务器操作

概述

服务器操作是在服务器上执行的异步函数。它们可以从客户端和服务器组件调用,用于数据变异、表单提交和其他服务器端操作。

定义服务器操作

在服务器组件中

在异步函数内部使用 'use server' 指令:

// app/page.tsx (服务器组件)
export default function Page() {
  async function createPost(formData: FormData) {
    'use server'
    const title = formData.get('title') as string
    await db.post.create({ data: { title } })
  }

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

在单独文件中

在整个文件上标记 'use server'

// app/actions.ts
'use server'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  await db.post.create({ data: { title } })
}

export async function deletePost(id: string) {
  await db.post.delete({ where: { id } })
}

表单处理

基本表单

// app/actions.ts
'use server'

export async function submitContact(formData: FormData) {
  const name = formData.get('name') as string
  const email = formData.get('email') as string
  const message = formData.get('message') as string

  await db.contact.create({
    data: { name, email, message }
  })
}

// app/contact/page.tsx
import { submitContact } from '@/app/actions'

export default function ContactPage() {
  return (
    <form action={submitContact}>
      <input name="name" required />
      <input name="email" type="email" required />
      <textarea name="message" required />
      <button type="submit">发送</button>
    </form>
  )
}

使用验证(Zod)

// app/actions.ts
'use server'

import { z } from 'zod'

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
})

export async function signup(formData: FormData) {
  const parsed = schema.safeParse({
    email: formData.get('email'),
    password: formData.get('password'),
  })

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

  await createUser(parsed.data)
  return { success: true }
}

useFormState 钩子

处理表单状态和错误:

// app/signup/page.tsx
'use client'

import { useFormState } from 'react-dom'
import { signup } from '@/app/actions'

const initialState = {
  error: null,
  success: false,
}

export default function SignupPage() {
  const [state, formAction] = useFormState(signup, initialState)

  return (
    <form action={formAction}>
      <input name="email" type="email" />
      <input name="password" type="password" />
      {state.error && (
        <p className="text-red-500">{state.error}</p>
      )}
      <button type="submit">注册</button>
    </form>
  )
}

// app/actions.ts
'use server'

export async function signup(prevState: any, formData: FormData) {
  const email = formData.get('email') as string

  if (!email.includes('@')) {
    return { error: '无效邮箱', success: false }
  }

  await createUser({ email })
  return { error: null, success: true }
}

useFormStatus 钩子

显示提交期间的加载状态:

// components/submit-button.tsx
'use client'

import { useFormStatus } from 'react-dom'

export function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" disabled={pending}>
      {pending ? '提交中...' : '提交'}
    </button>
  )
}

// 在表单中使用
import { SubmitButton } from '@/components/submit-button'

export default function Form() {
  return (
    <form action={submitAction}>
      <input name="title" />
      <SubmitButton />
    </form>
  )
}

重新验证

revalidatePath

重新验证特定路径:

'use server'

import { revalidatePath } from 'next/cache'

export async function createPost(formData: FormData) {
  await db.post.create({ data: { ... } })

  // 重新验证帖子列表页面
  revalidatePath('/posts')

  // 重新验证动态路由
  revalidatePath('/posts/[slug]', 'page')

  // 重新验证 /posts 下的所有路径
  revalidatePath('/posts', 'layout')
}

revalidateTag

通过缓存标签重新验证:

// 使用标签获取
const posts = await fetch('https://api.example.com/posts', {
  next: { tags: ['posts'] }
})

// 服务器操作
'use server'

import { revalidateTag } from 'next/cache'

export async function createPost(formData: FormData) {
  await db.post.create({ data: { ... } })
  revalidateTag('posts')
}

操作后重定向

'use server'

import { redirect } from 'next/navigation'

export async function createPost(formData: FormData) {
  const post = await db.post.create({ data: { ... } })

  // 重定向到新帖子
  redirect(`/posts/${post.slug}`)
}

乐观更新

在操作完成时立即更新UI:

'use client'

import { useOptimistic } from 'react'
import { addTodo } from '@/app/actions'

export function TodoList({ todos }: { todos: Todo[] }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, newTodo: string) => [
      ...state,
      { id: 'temp', title: newTodo, completed: false }
    ]
  )

  async function handleSubmit(formData: FormData) {
    const title = formData.get('title') as string
    addOptimisticTodo(title) // 立即更新UI
    await addTodo(formData)  // 服务器操作
  }

  return (
    <>
      <form action={handleSubmit}>
        <input name="title" />
        <button>添加</button>
      </form>
      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </>
  )
}

非表单使用

以编程方式调用服务器操作:

'use client'

import { deletePost } from '@/app/actions'

export function DeleteButton({ id }: { id: string }) {
  return (
    <button onClick={() => deletePost(id)}>
      删除
    </button>
  )
}

错误处理

'use server'

export async function createPost(formData: FormData) {
  try {
    await db.post.create({ data: { ... } })
    return { success: true }
  } catch (error) {
    if (error instanceof PrismaClientKnownRequestError) {
      if (error.code === 'P2002') {
        return { error: '已存在具有此标题的帖子' }
      }
    }
    return { error: '创建帖子失败' }
  }
}

安全考虑

  1. 始终验证输入 - 不要信任客户端数据
  2. 检查身份验证 - 验证用户是否授权
  3. 使用CSRF保护 - 服务器操作内置保护
  4. 消毒输出 - 防止XSS攻击
'use server'

import { auth } from '@/lib/auth'

export async function deletePost(id: string) {
  const session = await auth()

  if (!session) {
    throw new Error('未经授权')
  }

  const post = await db.post.findUnique({ where: { id } })

  if (post.authorId !== session.user.id) {
    throw new Error('禁止访问')
  }

  await db.post.delete({ where: { id } })
}

资源

有关详细模式,请参阅:

  • references/form-handling.md - 高级表单模式
  • references/revalidation.md - 缓存重新验证策略
  • examples/mutation-patterns.md - 完整变异示例