服务器操作和变更
服务器操作是在服务器上执行的 异步函数。它们可以在服务器和客户端组件中调用,来处理 Next.js 应用程序中的表单提交和数据变更。
🎥 观看: 通过服务器操作了解关于变更的更多信息。→ YouTube (10 分钟)。
公约
可以使用 React "use server" 指令来定义服务器操作。您可以将这个指令放置在async函数顶部来将这个函数标记为服务器操作,或者放在单个文件的顶部来将该文件中的所有 export 标记为服务器操作。
服务器组件
服务器组件可以使用内联函数级别或模块级别"use server"指令。要内联服务器操作,请将"use server"添加到函数体顶部:
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 "...";
}
客户端组件
要在客户端组件中调用服务器组件,要创建一个文件并将 "use server"指令添加到该文件上方。该文件内的所有函数将会被标记为可以在客户端和服务器组件内重复使用的服务器操作:
"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} />;
}
"use client";
import { create } from "@/app/actions";
export function Button() {
return <Button onClick={create} />;
}
将操作作为属性传递
您也可以将服务器操作作为属性传递给客户端组件:
<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 插件事实上并不知道它接收的是服务器操作还是普通函数。
运行时类型检查将会仍然确保您不会意外将函数传递给客户端组件。
行为
- 可以使用
<form>元素中的action属性来调用服务器操作:- 默认情况下,服务器组件支持渐进式增强,这意味着即使 Javascript 尚未被加载或被禁用,表单仍然会被提交。
- 在客户端组件中,如果 Javascript 还没有被加载,调用服务器操作的表单将会进行排队提交 forms,优先处理客户端激活。
- 激活后, 浏览器不会在提交表单时进行刷新。
- 服务器操作不限于
<form>操作,也涉及时间处理程序、useEffect、第三方库和其它表单元素,如:<button>。 - 服务器操作与 Next.js 缓存和重新验证 架构集成。调用操作时, Next.js 能够在一次服务器往返中返回更新后的 UI 和新数据。
- 在后台, actions 使用
POST方法, 并且只有 HTTP 方法可以调用它们。 - 服务器操作的参数和返回值必须可由 React 序列化. 参阅 React 文档以获得可序列化的参数和值的列表。
- 服务器操作是函数。这意味着它们可以在应用程序中的任何地方重复使用。
- 服务器操作从其所使用的页面或布局继承运行时。
- 服务器操作从其所使用的页面或布局继承路由段配置,包括字段,如
maxDuration。
例子
Forms
React 扩展 HTML <form> 元素,以便可以使用action 属性调用服务器操作。
当在表单中调用时, 操作自动接收FormData 对象。您不需要使用 React useState 来管理字段, 反而, 您可以使用原生的FormData 方法来提取数据:
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>;
}
您需要知道:
- 示例: 带有加载和错误状态的表单
- 当处理包含许多字段的表单时,您可以考虑使用带有 JavaScript
Object.fromEntries()的entries()方法。 例如:const rawFormData = Object.fromEntries(formData)。需要注意的一点是formData包含附加的$ACTION_属性。- 参阅 React
<form>文档了解更多信息。
传递附加参数
您可以使用 JavaScript bind方法向服务器操作传递其它的参数。
"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参数:
"use server";
export async function updateUser(userId, formData) {}
您需要知道:
- 另一种方法是将参数作为表单中隐藏的 input 字段进行传递 (例如
<input type="hidden" name="userId" value={userId} />)。不过, 该值将会成为渲染 HTML 的一部分,并且不会被编码。.bind适用于服务器和客户端组件。它也支持渐进增强。
嵌套表单元素
您也可以在嵌套在<form>内的元素中调用服务器操作,如<button>、 <input type="submit">和 <input type="image">。这些元素接收formAction属性或者事件处理程序。
这在您想在一个表单中调用多个服务器操作时非常有用。例如,除了发布帖子草稿,您可以创建一个特定的<button>元素来保存帖子草稿。参阅React <form> 文档来获得更多信息。
编程式表单提交
您可以使用requestSubmit()方法来以编程式触发表单提交。例如,当用户使用⌘ + Enter快捷键提交表单,您可以监听onKeyDown事件:
"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>原型提交,从而调用服务器操作。
服务器端表单验证
对于基本的客户端表单验证,我们推荐使用像 required 和 type="email"进行 HTML 验证。
对于更高级的服务器端验证, 在更改数据之前您可以使用库(如:zod)验证表单字段:
"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 useActionState hook 向用户展示 message。
- 通过向
useActionState传递操作, 该操作的函数签名会发生变化来接收一个新的prevState或initialState参数作为其第一个参数。 useActionState是一个 React hook,因此必须在客户端组件中使用。
"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");
}
然后, 您能够向useActionState hook 传递操作并使用返回的state来展示错误消息。
"use client";
import { useActionState } from "react";
import { createUser } from "@/app/actions";
const initialState = {
message: "",
};
export function Signup() {
const [state, formAction] = useActionState(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 { useActionState } from "react";
import { createUser } from "@/app/actions";
const initialState = {
message: "",
};
export function Signup() {
const [state, formAction] = useActionState(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 Canary 版本中, 该 API 是 React DOM 的一部分,被称为
useFormState。
待定状态
useActionState hook 暴露了pending状态,可用于执行操作时显示加载指示器。
"use client";
import { useActionState } from "react";
import { createUser } from "@/app/actions";
const initialState = {
message: "",
};
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState);
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
<button aria-disabled={pending} type="submit">
{pending ? "Submitting..." : "Sign up"}
</button>
</form>
);
}
"use client";
import { useActionState } from "react";
import { createUser } from "@/app/actions";
const initialState = {
message: "",
};
export function Signup() {
const [state, formAction, pending] = useActionState(createUser, initialState);
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
<button aria-disabled={pending} type="submit">
{pending ? "Submitting..." : "Sign up"}
</button>
</form>
);
}
您需要知道: 或者, 您也可以使用
useFormStatushook 为特定表单显示 pending 状态。
Optimistic updates
您可以在服务器操作完成执行之前使用 React useOptimistic hook 来乐观地更新 UI, 而不是等待响应:
"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>
);
}
事件处理程序
尽管在<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>
</>
);
}
"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 ':
"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 hook 来调用服务器操作。这对于依赖于全局事件或需要被自动触发的 mutations 非常有用。例如,app 快捷键onKeyDown、用于无限滚动的交叉观察器钩子或当组件挂载以更新视图计数时:
"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的行为和注意事项。
错误处理
当抛出错误时, 它将会被客户端上最近的error.js 或 <Suspense> 边界捕获。我们推荐使用try/catch返回由 UI 处理的错误。
例如, 您的服务器操作可以通过返回一条消息来处理创建新项时出现的错误:
"use server";
export async function createTodo(prevState: any, formData: FormData) {
try {
// Mutate data
} catch (e) {
throw new Error("Failed to create task");
}
}
"use server";
export async function createTodo(prevState, formData) {
try {
// Mutate data
} catch (e) {
throw new Error("Failed to create task");
}
}
您需要知道:
- 除了抛出错误, 您也可以返回一个由
useActionState处理的对象。参阅 服务器端验证和错误处理.
重新验证数据
您可以在服务器操作中使用revalidatePath API 重新验证Next.js 缓存:
"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来使带有缓存标签的特定数据获取无效:
"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");
}
重定向
如果您想在服务器操作完成后将用户重定向到不同的路由, 您可以使用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"); // 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 来get、 set和delete cookies:
"use server";
import { cookies } from "next/headers";
export async function exampleAction() {
// Get cookie
const value = cookies().get("name")?.value;
// Set cookie
cookies().set("name", "Delba");
// Delete cookie
cookies().delete("name");
}
"use server";
import { cookies } from "next/headers";
export async function exampleAction() {
// Get cookie
const value = cookies().get("name")?.value;
// Set cookie
cookies().set("name", "Delba");
// Delete cookie
cookies().delete("name");
}
参阅从服务器操作中删除 cookies 的 其它示例 。
Security
身份验证和授权
您应该将服务器操作视为面向公众的 API 端点, 并确保用户有权执行操作。例如:
"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>
);
}
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 应用程序时,会为每个操作生成一个秘密密钥。这意味着只能为指定的 build 调用操作。
您需要知道: 我们不推荐仅依赖加密来预防将敏感 值暴露到客户端。相反,您应该使用React taint APIs来主动阻止特定数据发送到客户端。
重写加密秘钥 (高级)
跨多个服务器自托管 Next.js 应用程序时,每个服务器实例最终可能使用不同的加密密钥,从而导致潜在的不一致。
为了缓解这种情况,您可以使用process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY环境变量重写加密秘钥。指定此变量可确保您的加密密钥在各个构建中都是不变的,并且所有服务器实例都使用相同的密钥。
这是一个高级用例,其中跨多个部署的一致加密行为对应用程序非常重要。您应该考虑标准的安全做法,例如密钥轮换和签名。
你需要知道: 部署到 Vercel 的 Next.js 应用程序会自动处理这个问题。
Allowed origins (高级)
因为可以在<form>元素中调用服务器操作,这使它们容易受到CSRF 攻击。
在后台, 服务器操作使用POST方法, 并且仅允许此 HTTP 方法调用它们,这防止了现代浏览器中的大多数的 CSRF 漏洞, 尤其是SameSite cookies 是默认设置。
作为额外的保护, Next.js 中的服务器操作也会也会比较Origin header和 Host header (或 X-Forwarded-Host)。如果这些不匹配, 请求将被终端。换句话说,服务器操作只能在托管它的页面相同的主机上调用。
对于使用反向代理或多层后端架构的大型应用程序(其中服务器 API 与生产域的不同),推荐使用配置选项serverActions.allowedOrigins选项来指定安全 origins 列表。该选项接收字符串数组。
/** @type {import('next').NextConfig} */
module.exports = {
experimental: {
serverActions: {
allowedOrigins: ["my-proxy.com", "*.my-proxy.com"],
},
},
};
了解有关安全和服务器操作的更多信息。
其它资源
有关 Server Actions 的更多信息,请查看以下 React 文档: