服务端操作与数据变更

服务端操作 (Server Actions) 是在服务端执行的异步函数。它们可以在服务端和客户端组件中调用,用于处理 Next.js 应用中的表单提交和数据变更。

🎥 观看视频: 了解更多关于服务端操作的数据变更 → YouTube (10分钟)

约定

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

服务端组件

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

export default function Page() {
  // 服务端操作
  async function create() {
    'use server'
    // 变更数据
  }

  return '...'
}

客户端组件

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

'use server'

export async function create() {}

将操作作为属性传递

你也可以将服务端操作作为属性传递给客户端组件:

<ClientComponent updateItemAction={updateItem} />
'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 结尾的属性会被假定为接收服务端操作。这只是一种启发式方法,因为 TypeScript 插件实际上并不知道它接收的是服务端操作还是普通函数。运行时类型检查仍会确保你不会意外将函数传递给客户端组件。

行为

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

示例

事件处理程序

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

'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 用于应用快捷键,交叉观察器钩子用于无限滚动,或在组件挂载时更新视图计数:

'use client'

import { incrementViews } from './actions'
import { useState, useEffect, useTransition } from 'react'

export default function ViewCount({ initialViews }: { initialViews: number }) {
  const [views, setViews] = useState(initialViews)
  const [isPending, startTransition] = useTransition()

  useEffect(() => {
    startTransition(async () => {
      const updatedViews = await incrementViews()
      setViews(updatedViews)
    })
  }, [])

  // 可以使用 `isPending` 向用户提供反馈
  return <p>Total Views: {views}</p>
}

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

错误处理

当抛出错误时,它将被客户端最近的 error.js<Suspense> 边界捕获。有关更多信息,请参阅错误处理

须知:

  • 除了抛出错误,你还可以返回一个对象供 useActionState 处理。

重新验证数据

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

'use server'

import { revalidatePath } from 'next/cache'

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidatePath('/posts')
}

或者使用 revalidateTag 通过缓存标签使特定数据获取无效:

'use server'

import { revalidateTag } from 'next/cache'

export async function createPost() {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidateTag('posts')
}

重定向

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

'use server'

import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'

export async function createPost(id: string) {
  try {
    // ...
  } catch (error) {
    // ...
  }

  revalidateTag('posts') // 更新缓存的帖子
  redirect(`/post/${id}`) // 导航到新帖子页面
}

Cookies

你可以使用 cookies API 在服务端操作中 getsetdelete cookies:

'use server'

import { cookies } from 'next/headers'

export async function exampleAction() {
  const cookieStore = await cookies()

  // 获取 cookie
  cookieStore.get('name')?.value

  // 设置 cookie
  cookieStore.set('name', 'Delba')

  // 删除 cookie
  cookieStore.delete('name')
}

有关从服务端操作删除 cookies 的更多示例,请参阅附加示例

安全性

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

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

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

须知:

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

// app/actions.js
'use server'

// 此操作**已**在我们的应用中使用,因此 Next.js
// 将创建一个安全 ID 以允许客户端引用
// 并调用服务端操作。
export async function updateUserAction(formData) {}

// 此操作**未**在我们的应用中使用,因此 Next.js
// 将在 `next build` 期间自动删除此代码
// 并且不会创建公共端点。
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 变量:

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 taint APIs 主动防止特定数据发送到客户端。

覆盖加密密钥(高级用法)

在跨多台服务器自托管 Next.js 应用时,每个服务器实例可能会生成不同的加密密钥,从而导致潜在的不一致问题。

为解决此问题,您可以通过 process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY 环境变量覆盖加密密钥。指定该变量可确保加密密钥在构建过程中保持持久性,且所有服务器实例使用相同密钥。该变量 必须 使用 AES-GCM 加密。

这是需要跨多个部署保持加密行为一致性的高级使用场景。您应考虑采用密钥轮换和签名等标准安全实践。

须知: 部署到 Vercel 的 Next.js 应用会自动处理此问题。

允许的请求源(高级用法)

由于服务端操作 (Server Actions) 可以在 <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 文档:

On this page