Skip to main content

服务器操作和突变

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

¥Server Actions are asynchronous functions that are executed on the server. They can be called in Server and Client Components to handle form submissions and data mutations in Next.js applications.

🎥 监视:详细了解使用服务器操作 → YouTube(10 分钟) 的突变。

¥🎥 Watch: Learn more about mutations with Server Actions → YouTube (10 minutes).

惯例

¥Convention

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

¥A Server Action can be defined with the React "use server" directive. You can place the directive at the top of an async function to mark the function as a Server Action, or at the top of a separate file to mark all exports of that file as Server Actions.

服务器组件

¥Server Components

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

¥Server Components can use the inline function level or module level "use server" directive. To inline a Server Action, add "use server" to the top of the function body:

export default function Page() {
// Server Action
async function create() {
'use server'
// Mutate data
}

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

return '...'
}

客户端组件

¥Client Components

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

¥To call a Server Action in a Client Component, create a new file and add the "use server" directive at the top of it. All exported functions within the file will be marked as Server Actions that can be reused in both Client and Server Components:

'use server'

export async function create() {}
'use server'

export async function create() {}
'use client'

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

export function Button() {
return <button onClick={() => create()}>Create</button>
}
'use client'

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

export function Button() {
return <button onClick={() => create()}>Create</button>
}

将操作作为 props 传递

¥Passing actions as props

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

¥You can also pass a Server Action to a Client Component as a prop:

<ClientComponent updateItemAction={updateItem} />
'use client'

export default function ClientComponent({
updateItemAction,
}: {
updateItemAction: (formData: FormData) => void
}) {
return <form action={updateItemAction}></form>
}
'use client'

export default function ClientComponent({ updateItemAction }) {
return <form action={updateItemAction}></form>
}

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

¥Usually, the Next.js TypeScript plugin would flag updateItemAction in client-component.tsx since it is a function which generally can't be serialized across client-server boundaries. However, props named action or ending with Action are assumed to receive Server Actions. This is only a heuristic since the TypeScript plugin doesn't actually know if it receives a Server Action or an ordinary function. Runtime type-checking will still ensure you don't accidentally pass a function to a Client Component.

行为

¥Behavior

  • 可以使用 <form> 元素 中的 action 属性来调用服务器操作:

    ¥Server actions can be invoked using the action attribute in a <form> element:

    • 服务器组件默认支持渐进增强,这意味着即使 JavaScript 尚未加载或被禁用,表单也会被提交。

      ¥Server Components support progressive enhancement by default, meaning the form will be submitted even if JavaScript hasn't loaded yet or is disabled.

    • 在客户端组件中,如果尚未加载 JavaScript,则调用服务器操作的表单将对提交进行排队,从而优先考虑客户端水合作用。

      ¥In Client Components, forms invoking Server Actions will queue submissions if JavaScript isn't loaded yet, prioritizing client hydration.

    • 水合后,浏览器在提交表单时不会刷新。

      ¥After hydration, the browser does not refresh on form submission.

  • 服务器操作不限于 <form>,可以从事件处理程序、useEffect、第三方库和其他表单元素(如 <button>)调用。

    ¥Server Actions are not limited to <form> and can be invoked from event handlers, useEffect, third-party libraries, and other form elements like <button>.

  • 服务器操作与 Next.js 缓存和重新验证 架构集成。调用操作时,Next.js 可以在单个服务器往返中返回更新的 UI 和新数据。

    ¥Server Actions integrate with the Next.js caching and revalidation architecture. When an action is invoked, Next.js can return both the updated UI and new data in a single server roundtrip.

  • 在幕后,操作使用 POST 方法,并且只有此 HTTP 方法可以调用它们。

    ¥Behind the scenes, actions use the POST method, and only this HTTP method can invoke them.

  • 服务器操作的参数和返回值必须可由 React 序列化。请参阅 React 文档以获取 可序列化的参数和值 的列表。

    ¥The arguments and return value of Server Actions must be serializable by React. See the React docs for a list of serializable arguments and values.

  • 服务器操作是函数。这意味着它们可以在你的应用中的任何位置重复使用。

    ¥Server Actions are functions. This means they can be reused anywhere in your application.

  • 服务器操作从它们所使用的页面或布局继承 runtime

    ¥Server Actions inherit the runtime from the page or layout they are used on.

  • 服务器操作从它们所使用的页面或布局继承 路由片段配置,包括 maxDuration 等字段。

    ¥Server Actions inherit the Route Segment Config from the page or layout they are used on, including fields like maxDuration.

示例

¥Examples

表单

¥Forms

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

¥React extends the HTML <form> element to allow Server Actions to be invoked with the action prop.

当在表单中调用时,该操作会自动接收 FormData 对象。你不需要使用 React useState 来管理字段,而是可以使用原生 FormData 方法 提取数据:

¥When invoked in a form, the action automatically receives the FormData object. You don't need to use React useState to manage fields, instead, you can extract the data using the native FormData methods:

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>
}
export default function Page() {
async function createInvoice(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>
}

很高兴知道:

¥Good to know:

传递其他参数

¥Passing additional arguments

你可以使用 JavaScript bind 方法将其他参数传递给服务器操作。

¥You can pass additional arguments to a Server Action using the JavaScript bind method.

'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>
)
}
'use client'

import { updateUser } from './actions'

export function UserProfile({ userId }) {
const updateUserWithId = updateUser.bind(null, userId)

return (
<form action={updateUserWithId}>
<input type="text" name="name" />
<button type="submit">Update User Name</button>
</form>
)
}

除了表单数据之外,服务器操作还将接收 userId 参数:

¥The Server Action will receive the userId argument, in addition to the form data:

'use server'

export async function updateUser(userId: string, formData: FormData) {}
'use server'

export async function updateUser(userId, formData) {}

很高兴知道:

¥Good to know:

  • 另一种方法是将参数作为表单中的隐藏输入字段传递(例如 <input type="hidden" name="userId" value={userId} />)。但是,该值将成为渲染的 HTML 的一部分,并且不会被编码。

    ¥An alternative is to pass arguments as hidden input fields in the form (e.g. <input type="hidden" name="userId" value={userId} />). However, the value will be part of the rendered HTML and will not be encoded.

  • .bind 适用于服务器和客户端组件。它还支持渐进增强。

    ¥.bind works in both Server and Client Components. It also supports progressive enhancement.

嵌套表单元素

¥Nested form elements

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

¥You can also invoke a Server Action in elements nested inside <form> such as <button>, <input type="submit">, and <input type="image">. These elements accept the formAction prop or event handlers.

当你想要在表单中调用多个服务器操作时,这非常有用。例如,你可以创建一个特定的 <button> 元素来保存帖子草稿以及发布它。请参阅 React <form> 文档 了解更多信息。

¥This is useful in cases where you want to call multiple server actions within a form. For example, you can create a specific <button> element for saving a post draft in addition to publishing it. See the React <form> docs for more information.

程序化表单提交

¥Programmatic form submission

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

¥You can trigger a form submission programmatically using the requestSubmit() method. For example, when the user submits a form using the + Enter keyboard shortcut, you can listen for the onKeyDown event:

'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>
)
}
'use client'

export function Entry() {
const handleKeyDown = (e) => {
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> 祖级的提交,这将调用服务器操作。

¥This will trigger the submission of the nearest <form> ancestor, which will invoke the Server Action.

服务器端表单验证

¥Server-side form validation

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

¥You can use the HTML attributes like required and type="email" for basic client-side form validation.

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

¥For more advanced server-side validation, you can use a library like zod to validate the form fields before mutating the data:

'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
}
'use server'

import { z } from 'zod'

const schema = z.object({
email: z.string({
invalid_type_error: 'Invalid Email',
}),
})

export default async function createsUser(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 钩子向用户显示消息。

¥Once the fields have been validated on the server, you can return a serializable object in your action and use the React useFormState hook to show a message to the user.

  • 通过将操作传递给 useFormState,操作的函数签名将更改为接收新的 prevStateinitialState 参数作为其第一个参数。

    ¥By passing the action to useFormState, the action's function signature changes to receive a new prevState or initialState parameter as its first argument.

  • useFormState 是一个 React hook,因此必须在客户端组件中使用。

    ¥useFormState is a React hook and therefore must be used in a Client Component.

'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')
}
'use server'

import { redirect } from 'next/navigation'

export async function createUser(prevState, 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 显示错误消息。

¥Then, you can pass your action to the useFormState hook and use the returned state to display an error message.

'use client'

import { useFormState } from 'react-dom'
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>
)
}
'use client'

import { useFormState } from 'react-dom'
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>
)
}

很高兴知道:

¥Good to know:

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

    ¥These examples use React's useFormState hook, which is bundled with the Next.js App Router. If you are using React 19, use useActionState instead. See the React docs for more information.

待定状态

¥Pending states

  • 在更改数据之前,你应始终确保用户也有权执行该操作。参见 认证与授权

    ¥Before mutating data, you should always ensure a user is also authorized to perform the action. See Authentication and Authorization.

useFormStatus 钩子公开了一个 pending 布尔值,可用于在执行操作时显示加载指示器。

¥The useFormStatus hook exposes a pending boolean that can be used to show a loading indicator while the action is being executed.

'use client'

import { useFormStatus } from 'react-dom'

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

return (
<button disabled={pending} type="submit">
Sign Up
</button>
)
}
'use client'

import { useFormStatus } from 'react-dom'

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

return (
<button disabled={pending} type="submit">
Sign Up
</button>
)
}

很高兴知道:

¥Good to know:

  • 在 React 19 中,useFormStatus 在返回的对象上包含其他键,如数据、方法和操作。如果你没有使用 React 19,则只有 pending 键可用。

    ¥In React 19, useFormStatus includes additional keys on the returned object, like data, method, and action. If you are not using React 19, only the pending key is available.

  • 在 React 19 中,useActionState 还在返回状态中包含一个 pending 键。

    ¥In React 19, useActionState also includes a pending key on the returned state.

乐观的更新

¥Optimistic updates

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

¥You can use the React useOptimistic hook to optimistically update the UI before the Server Action finishes executing, rather than waiting for the response:

'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>
)
}
'use client'

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

export function Thread({ messages }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [...state, { message: newMessage }]
)

const formAction = async (formData) => {
const message = formData.get('message')
addOptimisticMessage(message)
await send(message)
}

return (
<div>
{optimisticMessages.map((m) => (
<div>{m.message}</div>
))}
<form action={formAction}>
<input type="text" name="message" />
<button type="submit">Send</button>
</form>
</div>
)
}

事件处理程序

¥Event handlers

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

¥While it's common to use Server Actions within <form> elements, they can also be invoked with event handlers such as onClick. For example, to increment a like count:

'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>
</>
)
}
'use client'

import { incrementLike } from './actions'
import { useState } from 'react'

export default function LikeButton({ initialLikes }) {
const [likes, setLikes] = useState(initialLikes)

return (
<>
<p>Total Likes: {likes}</p>
<button
onClick={async () => {
const updatedLikes = await incrementLike()
setLikes(updatedLikes)
}}
>
Like
</button>
</>
)
}

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

¥You can also add event handlers to form elements, for example, to save a form field onChange:

'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>
)
}

对于这样的情况,可能会快速连续触发多个事件,我们建议进行反跳以防止不必要的服务器操作调用。

¥For cases like this, where multiple events might be fired in quick succession, we recommend debouncing to prevent unnecessary Server Action invocations.

useEffect

当组件安装或依赖更改时,你可以使用 React useEffect 钩子来调用服务器操作。这对于依赖全局事件或需要自动触发的突变非常有用。例如,onKeyDown 用于应用快捷方式,用于无限滚动的交叉观察者钩子,或者当组件安装以更新视图计数时:

¥You can use the React useEffect hook to invoke a Server Action when the component mounts or a dependency changes. This is useful for mutations that depend on global events or need to be triggered automatically. For example, onKeyDown for app shortcuts, an intersection observer hook for infinite scrolling, or when the component mounts to update a view count:

'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>
}
'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

¥Remember to consider the behavior and caveats of useEffect.

错误处理

¥Error Handling

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

¥When an error is thrown, it'll be caught by the nearest error.js or <Suspense> boundary on the client. See Error Handling for more information.

很高兴知道:

¥Good to know:

重新验证数据

¥Revalidating data

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

¥You can revalidate the Next.js Cache inside your Server Actions with the revalidatePath API:

'use server'

import { revalidatePath } from 'next/cache'

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

revalidatePath('/posts')
}
'use server'

import { revalidatePath } from 'next/cache'

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

revalidatePath('/posts')
}

或者使用 revalidateTag 使带有缓存标记的特定数据获取无效:

¥Or invalidate a specific data fetch with a cache tag using revalidateTag:

'use server'

import { revalidateTag } from 'next/cache'

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

revalidateTag('posts')
}
'use server'

import { revalidateTag } from 'next/cache'

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

revalidateTag('posts')
}

重定向

¥Redirecting

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

¥If you would like to redirect the user to a different route after the completion of a Server Action, you can use redirect API. redirect needs to be called outside of the try/catch block:

'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
}
'use server'

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

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

revalidateTag('posts') // Update cached posts
redirect(`/post/${id}`) // Navigate to the new post page
}

Cookies

你可以使用 cookies API 在服务器操作中设置 getsetdelete cookie:

¥You can get, set, and delete cookies inside a Server Action using the cookies API:

'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')
}
'use server'

import { cookies } from 'next/headers'

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

// Get cookie
cookieStore.get('name')?.value

// Set cookie
cookieStore.set('name', 'Delba')

// Delete cookie
cookieStore.delete('name')
}

请参阅 附加示例 以了解从服务器操作中删除 cookie。

¥See additional examples for deleting cookies from Server Actions.

安全

¥Security

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

¥By default, when a Server Action is created and exported, it creates a public HTTP endpoint and should be treated with the same security assumptions and authorization checks. This means, even if a Server Action or utility function is not imported elsewhere in your code, it’s still publicly accessible.

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

¥To improve security, Next.js has the following built-in features:

  • 安全操作 ID:Next.js 创建加密的非确定性 ID,以允许客户端引用和调用服务器操作。为了增强安全性,这些 ID 会在构建之间定期重新计算。

    ¥Secure action IDs: Next.js creates encrypted, non-deterministic IDs to allow the client to reference and call the Server Action. These IDs are periodically recalculated between builds for enhanced security.

  • 死代码消除:未使用的服务器操作(由其 ID 引用)将从客户端包中删除,以避免第三方公开访问。

    ¥Dead code elimination: Unused Server Actions (referenced by their IDs) are removed from client bundle to avoid public access by third-party.

很高兴知道:

¥Good to know:

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

¥The IDs are created during compilation and are cached for a maximum of 14 days. They will be regenerated when a new build is initiated or when the build cache is invalidated. This security improvement reduces the risk in cases where an authentication layer is missing. However, you should still treat Server Actions like public HTTP endpoints.

// 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) {}

认证与授权

¥Authentication and authorization

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

¥You should ensure that the user is authorized to perform the action. For example:

'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')
}

// ...
}

闭包和加密

¥Closures and encryption

在组件内部定义服务器操作会创建一个 closure,其中该操作可以访问外部函数的范围。例如,publish 操作可以访问 publishVersion 变量:

¥Defining a Server Action inside a component creates a closure where the action has access to the outer function's scope. For example, the publish action has access to the publishVersion variable:

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>
);
}
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)以便稍后在调用操作时使用它时,闭包非常有用。

¥Closures are useful when you need to capture a snapshot of data (e.g. publishVersion) at the time of rendering so that it can be used later when the action is invoked.

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

¥However, for this to happen, the captured variables are sent to the client and back to the server when the action is invoked. To prevent sensitive data from being exposed to the client, Next.js automatically encrypts the closed-over variables. A new private key is generated for each action every time a Next.js application is built. This means actions can only be invoked for a specific build.

很高兴知道:我们不建议仅依靠加密来防止敏感值暴露在客户端上。相反,你应该使用 React 污点 API 主动阻止特定数据发送到客户端。

¥Good to know: We don't recommend relying on encryption alone to prevent sensitive values from being exposed on the client. Instead, you should use the React taint APIs to proactively prevent specific data from being sent to the client.

覆盖加密密钥(高级)

¥Overwriting encryption keys (advanced)

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

¥When self-hosting your Next.js application across multiple servers, each server instance may end up with a different encryption key, leading to potential inconsistencies.

为了缓解这种情况,你可以使用 process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY 环境变量覆盖加密密钥。指定此变量可确保你的加密密钥在各个版本中保持不变,并且所有服务器实例都使用相同的密钥。

¥To mitigate this, you can overwrite the encryption key using the process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY environment variable. Specifying this variable ensures that your encryption keys are persistent across builds, and all server instances use the same key.

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

¥This is an advanced use case where consistent encryption behavior across multiple deployments is critical for your application. You should consider standard security practices such key rotation and signing.

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

¥Good to know: Next.js applications deployed to Vercel automatically handle this.

允许的来源(高级)

¥Allowed origins (advanced)

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

¥Since Server Actions can be invoked in a <form> element, this opens them up to CSRF attacks.

在幕后,服务器操作使用 POST 方法,并且只有此 HTTP 方法才允许调用它们。这可以防止现代浏览器中的大多数 CSRF 漏洞,特别是在 同站点 cookies 为默认浏览器的情况下。

¥Behind the scenes, Server Actions use the POST method, and only this HTTP method is allowed to invoke them. This prevents most CSRF vulnerabilities in modern browsers, particularly with SameSite cookies being the default.

作为额外的保护,Next.js 中的服务器操作还会将 来源标头主机头(或 X-Forwarded-Host)进行比较。如果这些不匹配,请求将被中止。换句话说,服务器操作只能在与承载它的页面相同的主机上调用。

¥As an additional protection, Server Actions in Next.js also compare the Origin header to the Host header (or X-Forwarded-Host). If these don't match, the request will be aborted. In other words, Server Actions can only be invoked on the same host as the page that hosts it.

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

¥For large applications that use reverse proxies or multi-layered backend architectures (where the server API differs from the production domain), it's recommended to use the configuration option serverActions.allowedOrigins option to specify a list of safe origins. The option accepts an array of strings.

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

了解有关 安全和服务器操作 的更多信息。

¥Learn more about Security and Server Actions.

其他资源

¥Additional resources

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

¥For more information, check out the following React docs: