跳至内容
构建您的应用程序数据获取服务器操作和变异

服务器操作和变异

服务器操作 是在服务器上执行的**异步函数**。它们可以在服务器和客户端组件中调用,以处理 Next.js 应用程序中的表单提交和数据变异。

🎥观看:了解有关使用服务器操作进行变异的更多信息 → YouTube (10 分钟)

约定

可以使用 React "use server" 指令来定义服务器操作。您可以将指令放在async函数的顶部以将其标记为服务器操作,或者放在单独文件的顶部以将该文件的所有导出标记为服务器操作。

服务器组件

服务器组件可以使用内联函数级别或模块级别的"use server"指令。要内联服务器操作,请将"use server"添加到函数体顶部

app/page.tsx
export default function Page() {
  // Server Action
  async function create() {
    'use server'
    // Mutate data
  }
 
  return '...'
}

客户端组件

要在客户端组件中调用服务器操作,请创建一个新文件并在其顶部添加"use server"指令。该文件中的所有导出函数都将被标记为服务器操作,可以在客户端和服务器组件中重复使用

app/actions.ts
'use server'
 
export async function create() {}
app/ui/button.tsx
'use client'
 
import { create } from '@/app/actions'
 
export function Button() {
  return <button onClick={() => create()}>Create</button>
}

将操作作为道具传递

您还可以将服务器操作作为道具传递给客户端组件

<ClientComponent updateItemAction={updateItem} />
app/client-component.tsx
'use client'
 
export default function ClientComponent({
  updateItemAction,
}: {
  updateItemAction: (formData: FormData) => void
}) {
  return <form action={updateItemAction}>{/* ... */}</form>
}

通常情况下,Next.js TypeScript 插件会在 client-component.tsx 中标记 updateItemAction,因为它是一个函数,通常无法在客户端和服务器之间序列化。但是,名为 action 或以 Action 结尾的 props 被假定为接收服务器操作。这只是一个启发式方法,因为 TypeScript 插件实际上并不知道它是否接收的是服务器操作还是普通函数。运行时类型检查仍然会确保您不会意外地将函数传递给客户端组件。

行为

  • 可以使用 <form> 元素 中的 action 属性调用服务器操作。
    • 服务器组件默认支持渐进增强,这意味着即使 JavaScript 尚未加载或已禁用,表单也会提交。
    • 在客户端组件中,如果 JavaScript 尚未加载,则调用服务器操作的表单将排队提交,优先考虑客户端水合。
    • 水合后,浏览器在表单提交时不会刷新。
  • 服务器操作不限于 <form>,可以从事件处理程序、useEffect、第三方库和其他表单元素(如 <button>)中调用。
  • 服务器操作与 Next.js 的 缓存和重新验证 架构集成。当调用操作时,Next.js 可以通过一次服务器往返返回更新后的 UI 和新数据。
  • 在幕后,操作使用 POST 方法,并且只有此 HTTP 方法可以调用它们。
  • 服务器操作的参数和返回值必须能够被 React 序列化。有关可序列化参数和值的列表,请参阅 React 文档 可序列化参数和值
  • 服务器操作是函数。这意味着它们可以在应用程序中的任何位置重复使用。
  • 服务器操作继承其所用页面或布局的 运行时
  • 服务器操作继承其所用页面或布局的 路由段配置,包括 maxDuration 等字段。

示例

表单

React 扩展了 HTML <form> 元素,允许使用 action 属性调用服务器操作。

在表单中调用时,操作会自动接收 FormData 对象。您无需使用 React useState 来管理字段,而是可以使用原生的 FormData 方法

app/invoices/page.tsx
export default function Page() {
  async function createInvoice(formData: FormData) {
    'use server'
 
    const rawFormData = {
      customerId: formData.get('customerId'),
      amount: formData.get('amount'),
      status: formData.get('status'),
    }
 
    // mutate data
    // revalidate cache
  }
 
  return <form action={createInvoice}>...</form>
}

了解一下

传递额外参数

可以使用 JavaScript 的 bind 方法将额外参数传递给服务器操作。

app/client-component.tsx
'use client'
 
import { updateUser } from './actions'
 
export function UserProfile({ userId }: { userId: string }) {
  const updateUserWithId = updateUser.bind(null, userId)
 
  return (
    <form action={updateUserWithId}>
      <input type="text" name="name" />
      <button type="submit">Update User Name</button>
    </form>
  )
}

除了表单数据外,服务器操作还会接收 userId 参数。

app/actions.js
'use server'
 
export async function updateUser(userId, formData) {}

了解一下:

  • 另一种方法是将参数作为隐藏的输入字段传递到表单中(例如 <input type="hidden" name="userId" value={userId} />)。但是,该值将成为渲染的 HTML 的一部分,并且不会被编码。
  • .bind 在服务器和客户端组件中都有效。它也支持渐进增强。

嵌套表单元素

您也可以在 <form> 内嵌套的元素中调用服务器操作,例如 <button><input type="submit"><input type="image">。这些元素接受 formAction 属性或 事件处理程序

这在您希望在一个表单中调用多个服务器操作的情况下很有用。例如,除了发布帖子之外,您还可以为保存帖子草稿创建一个特定的 <button> 元素。有关更多信息,请参阅 React <form> 文档

程序化表单提交

可以使用 requestSubmit() 方法以编程方式触发表单提交。例如,当用户使用 + Enter 键盘快捷键提交表单时,您可以侦听 onKeyDown 事件。

app/entry.tsx
'use client'
 
export function Entry() {
  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
    if (
      (e.ctrlKey || e.metaKey) &&
      (e.key === 'Enter' || e.key === 'NumpadEnter')
    ) {
      e.preventDefault()
      e.currentTarget.form?.requestSubmit()
    }
  }
 
  return (
    <div>
      <textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
    </div>
  )
}

这将触发最近的 <form> 父级的提交,从而调用服务器操作。

服务器端表单验证

您可以使用 HTML 属性(如 requiredtype="email")进行基本的客户端表单验证。

对于更高级的服务器端验证,您可以使用像 zod 这样的库在修改数据之前验证表单字段。

app/actions.ts
'use server'
 
import { z } from 'zod'
 
const schema = z.object({
  email: z.string({
    invalid_type_error: 'Invalid Email',
  }),
})
 
export default async function createUser(formData: FormData) {
  const validatedFields = schema.safeParse({
    email: formData.get('email'),
  })
 
  // Return early if the form data is invalid
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }
 
  // Mutate data
}

在服务器上验证字段后,您可以在操作中返回一个可序列化的对象,并使用 React 的 useFormState 钩子向用户显示消息。

  • 通过将操作传递给 useFormState,操作的函数签名将更改为在其第一个参数中接收一个新的 prevStateinitialState 参数。
  • useFormState 是一个 React 钩子,因此必须在客户端组件中使用。
app/actions.ts
'use server'
 
import { redirect } from 'next/navigation'
 
export async function createUser(prevState: any, formData: FormData) {
  const res = await fetch('https://...')
  const json = await res.json()
 
  if (!res.ok) {
    return { message: 'Please enter a valid email' }
  }
 
  redirect('/dashboard')
}

然后,您可以将操作传递给 useFormState 钩子,并使用返回的 state 显示错误消息。

app/ui/signup.tsx
'use client'
 
import { useFormState } from 'react'
import { createUser } from '@/app/actions'
 
const initialState = {
  message: '',
}
 
export function Signup() {
  const [state, formAction] = useFormState(createUser, initialState)
 
  return (
    <form action={formAction}>
      <label htmlFor="email">Email</label>
      <input type="text" id="email" name="email" required />
      {/* ... */}
      <p aria-live="polite">{state?.message}</p>
      <button>Sign up</button>
    </form>
  )
}

了解一下

  • 这些示例使用了 React 的 useFormState 钩子,它与 Next.js App Router 捆绑在一起。如果您使用的是 React 19,请改用 useActionState。请参阅 React 文档 以获取更多信息。

待处理状态

  • 在修改数据之前,您应该始终确保用户也具有执行该操作的权限。请参阅 身份验证和授权

useFormStatus 钩子公开了 pending 布尔值,可以在执行操作时使用它来显示加载指示器。请参阅 React 文档

app/submit-button.tsx
'use client'
 
import { useFormStatus } from 'react-dom'
 
export function SubmitButton() {
  const { pending } = useFormStatus()
 
  return (
    <button disabled={pending} type="submit">
      Sign Up
    </button>
  )
}

了解一下

  • 在 React 19 中,useFormStatus 在返回的对象上包含其他键,例如 data、method 和 action。如果您未使用 React 19,则仅 pending 键可用。
  • 在 React 19 中,useActionState 也在返回的状态上包含 pending 键。

乐观更新

您可以使用 React 的 useOptimistic 钩子来在服务器操作完成执行之前乐观地更新 UI,而不是等待响应。

app/page.tsx
'use client'
 
import { useOptimistic } from 'react'
import { send } from './actions'
 
type Message = {
  message: string
}
 
export function Thread({ messages }: { messages: Message[] }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic<
    Message[],
    string
  >(messages, (state, newMessage) => [...state, { message: newMessage }])
 
  const formAction = async (formData) => {
    const message = formData.get('message') as string
    addOptimisticMessage(message)
    await send(message)
  }
 
  return (
    <div>
      {optimisticMessages.map((m, i) => (
        <div key={i}>{m.message}</div>
      ))}
      <form action={formAction}>
        <input type="text" name="message" />
        <button type="submit">Send</button>
      </form>
    </div>
  )
}

事件处理程序

虽然在 <form> 元素中使用服务器操作很常见,但它们也可以通过事件处理程序(如 onClick)来调用。例如,要增加点赞数

app/like-button.tsx
'use client'
 
import { incrementLike } from './actions'
import { useState } from 'react'
 
export default function LikeButton({ initialLikes }: { initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes)
 
  return (
    <>
      <p>Total Likes: {likes}</p>
      <button
        onClick={async () => {
          const updatedLikes = await incrementLike()
          setLikes(updatedLikes)
        }}
      >
        Like
      </button>
    </>
  )
}

您还可以向表单元素添加事件处理程序,例如,在 onChange 时保存表单字段。

app/ui/edit-post.tsx
'use client'
 
import { publishPost, saveDraft } from './actions'
 
export default function EditPost() {
  return (
    <form action={publishPost}>
      <textarea
        name="content"
        onChange={async (e) => {
          await saveDraft(e.target.value)
        }}
      />
      <button type="submit">Publish</button>
    </form>
  )
}

对于像这种情况,多个事件可能在快速连续地触发,我们建议使用 **防抖** 来防止不必要的服务器操作调用。

useEffect

您可以使用 React 的 useEffect 钩子在组件挂载或依赖项更改时调用服务器操作。这对于依赖于全局事件或需要自动触发的变异很有用。例如,应用快捷键的 onKeyDown,无限滚动的交集观察器钩子,或者组件挂载时更新查看次数。

app/view-count.tsx
'use client'
 
import { incrementViews } from './actions'
import { useState, useEffect } from 'react'
 
export default function ViewCount({ initialViews }: { initialViews: number }) {
  const [views, setViews] = useState(initialViews)
 
  useEffect(() => {
    const updateViews = async () => {
      const updatedViews = await incrementViews()
      setViews(updatedViews)
    }
 
    updateViews()
  }, [])
 
  return <p>Total Views: {views}</p>
}

请记住考虑 useEffect 的行为和注意事项

错误处理

当抛出错误时,它将被客户端上最近的 error.js<Suspense> 边界捕获。我们建议使用 try/catch 将错误返回以由您的 UI 处理。

例如,您的服务器操作可能会通过返回消息来处理创建新项目时的错误。

app/actions.ts
'use server'
 
export async function createTodo(prevState: any, formData: FormData) {
  try {
    // Mutate data
  } catch (e) {
    throw new Error('Failed to create task')
  }
}

了解一下

重新验证数据

您可以使用 revalidatePath API 在服务器操作中重新验证 Next.js 缓存

app/actions.ts
'use server'
 
import { revalidatePath } from 'next/cache'
 
export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }
 
  revalidatePath('/posts')
}

或者使用 revalidateTag 使使用缓存标签的特定数据获取失效。

app/actions.ts
'use server'
 
import { revalidateTag } from 'next/cache'
 
export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }
 
  revalidateTag('posts')
}

重定向

如果您希望在服务器操作完成后将用户重定向到不同的路由,可以使用 redirect API。redirect 需要在 try/catch 代码块之外调用。

app/actions.ts
'use server'
 
import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
 
export async function createPost(id: string) {
  try {
    // ...
  } catch (error) {
    // ...
  }
 
  revalidateTag('posts') // Update cached posts
  redirect(`/post/${id}`) // Navigate to the new post page
}

Cookie

您可以使用 cookies API 在服务器操作中 getsetdelete Cookie。

app/actions.ts
'use server'
 
import { cookies } from 'next/headers'
 
export async function exampleAction() {
  const cookieStore = await cookies()
 
  // Get cookie
  cookieStore.get('name')?.value
 
  // Set cookie
  cookieStore.set('name', 'Delba')
 
  // Delete cookie
  cookieStore.delete('name')
}

请参阅 其他示例,了解如何从服务器操作中删除 Cookie。

安全性

默认情况下,当创建和导出服务器操作时,它会创建一个公共 HTTP 端点,并且应以相同的安全假设和授权检查来对待。这意味着,即使服务器操作或实用程序函数在您的代码中没有其他地方导入,它仍然是公开可访问的。

为了提高安全性,Next.js 具有以下内置功能:

  • 安全操作 ID:Next.js 创建加密的、非确定性的 ID,以允许客户端引用和调用服务器操作。这些 ID 会在构建之间定期重新计算,以增强安全性。
  • 死代码消除:未使用的服务器操作(通过其 ID 引用)将从客户端包中删除,以避免第三方公开访问。

了解一下:

这些 ID 在编译期间创建,并缓存最长 14 天。当启动新的构建或构建缓存失效时,它们将被重新生成。此安全改进降低了缺少身份验证层的情况下的风险。但是,您仍然应该将服务器操作视为公共 HTTP 端点。

// app/actions.js
'use server'
 
// This action **is** used in our application, so Next.js
// will create a secure ID to allow the client to reference
// and call the Server Action.
export async function updateUserAction(formData) {}
 
// This action **is not** used in our application, so Next.js
// will automatically remove this code during `next build`
// and will not create a public endpoint.
export async function deleteUserAction(formData) {}

身份验证和授权

您应该确保用户有权执行该操作。例如

app/actions.ts
'use server'
 
import { auth } from './lib'
 
export function addItem() {
  const { user } = auth()
  if (!user) {
    throw new Error('You must be signed in to perform this action')
  }
 
  // ...
}

闭包和加密

在组件内部定义服务器操作会创建一个 闭包,其中操作可以访问外部函数的作用域。例如,publish 操作可以访问 publishVersion 变量。

app/page.tsx
export default async function Page() {
  const publishVersion = await getLatestVersion();
 
  async function publish() {
    "use server";
    if (publishVersion !== await getLatestVersion()) {
      throw new Error('The version has changed since pressing publish');
    }
    ...
  }
 
  return (
    <form>
      <button formAction={publish}>Publish</button>
    </form>
  );
}

当您需要捕获数据的快照(例如 publishVersion)在渲染时,以便稍后在调用操作时使用时,闭包很有用。

然而,要使这种情况发生,捕获的变量会在调用操作时发送到客户端并返回到服务器。为了防止敏感数据暴露给客户端,Next.js 会自动加密闭包变量。每次构建 Next.js 应用程序时,都会为每个操作生成一个新的私钥。这意味着操作只能针对特定构建进行调用。

了解一下:我们不建议仅依赖加密来防止敏感值暴露在客户端。相反,您应该使用React 污染 API来主动防止特定数据发送到客户端。

覆盖加密密钥(高级)

当在多个服务器上自行托管 Next.js 应用程序时,每个服务器实例最终可能会使用不同的加密密钥,从而导致潜在的不一致性。

为了缓解这种情况,您可以使用process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY环境变量覆盖加密密钥。指定此变量可确保您的加密密钥在构建之间保持持久性,并且所有服务器实例都使用相同的密钥。

这是一个高级用例,其中跨多个部署的一致加密行为对于您的应用程序至关重要。您应该考虑标准的安全实践,例如密钥轮换和签名。

了解一下:部署到 Vercel 的 Next.js 应用程序会自动处理此问题。

允许的来源(高级)

由于服务器操作可以在<form>元素中调用,因此这使它们容易受到CSRF 攻击

在幕后,服务器操作使用POST方法,并且仅允许此 HTTP 方法调用它们。这可以防止大多数现代浏览器中的 CSRF 漏洞,特别是使用SameSite cookie作为默认值。

作为额外的保护措施,Next.js 中的服务器操作还会将Origin 标头Host 标头(或X-Forwarded-Host)进行比较。如果两者不匹配,则请求将被中止。换句话说,服务器操作只能在与其托管的页面相同的主机上调用。

对于使用反向代理或多层后端架构(其中服务器 API 与生产域不同)的大型应用程序,建议使用配置选项serverActions.allowedOrigins选项来指定安全来源列表。该选项接受字符串数组。

next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
  experimental: {
    serverActions: {
      allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
    },
  },
}

详细了解服务器操作的安全

其他资源

有关更多信息,请查看以下 React 文档

后续步骤

了解如何在 Next.js 中配置服务器操作