跳到主要内容

Server Actions

Server Actions 是 Next.js 提供的服务端函数,可以在服务端执行数据变更操作。

基本用法

定义 Server Action

在函数顶部添加 "use server" 指令:

// app/actions.ts
"use server";

import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";

export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;

await db.post.create({
data: { title, content },
});

revalidatePath("/posts");
}

在表单中使用

// app/posts/page.tsx
import { createPost } from "./actions";

export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" type="text" placeholder="标题" required />
<textarea name="content" placeholder="内容" required />
<button type="submit">发布</button>
</form>
);
}

表单处理

获取表单数据

"use server";

export async function submitForm(formData: FormData) {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
const age = parseInt(formData.get("age") as string);
const subscribed = formData.get("subscribe") === "on";

console.log({ name, email, age, subscribed });
}

多个表单字段

"use server";

export async function createOrder(formData: FormData) {
const items = formData.getAll("items") as string[];
const quantities = formData.getAll("quantities") as string[];

const orderItems = items.map((item, index) => ({
item,
quantity: parseInt(quantities[index]),
}));

await createOrderInDatabase(orderItems);
}

文件上传

"use server";

export async function uploadFile(formData: FormData) {
const file = formData.get("file") as File;

if (!file) {
return { error: "请选择文件" };
}

const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);

const filename = `${Date.now()}-${file.name}`;
await writeFile(`./uploads/${filename}`, buffer);

return { success: true, filename };
}

状态处理

使用 useFormState

// app/actions.ts
"use server";

export async function createPost(
prevState: { error?: string; success?: boolean },
formData: FormData
) {
const title = formData.get("title") as string;

if (!title || title.length < 3) {
return { error: "标题至少需要 3 个字符" };
}

try {
await db.post.create({ data: { title } });
return { success: true };
} catch (error) {
return { error: "创建失败" };
}
}
// app/posts/page.tsx
"use client";

import { useFormState } from "react-dom";
import { createPost } from "./actions";

export default function NewPostPage() {
const [state, formAction] = useFormState(createPost, {
error: undefined,
success: false,
});

return (
<form action={formAction}>
<input name="title" type="text" required />
{state.error && <p className="text-red-500">{state.error}</p>}
{state.success && <p className="text-green-500">创建成功</p>}
<button type="submit">提交</button>
</form>
);
}

使用 useFormStatus

"use client";

import { useFormStatus } from "react-dom";

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

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

export default function Form() {
return (
<form action={createPost}>
<input name="title" type="text" required />
<SubmitButton />
</form>
);
}

数据验证

使用 Zod 验证

// app/actions.ts
"use server";

import { z } from "zod";

const postSchema = z.object({
title: z.string().min(3, "标题至少 3 个字符").max(100),
content: z.string().min(10, "内容至少 10 个字符"),
category: z.enum(["tech", "life", "other"]),
});

export async function createPost(formData: FormData) {
const rawData = {
title: formData.get("title"),
content: formData.get("content"),
category: formData.get("category"),
};

const result = postSchema.safeParse(rawData);

if (!result.success) {
return {
error: result.error.flatten().fieldErrors,
};
}

await db.post.create({ data: result.data });
revalidatePath("/posts");
}

重新验证

重新验证路径

"use server";

import { revalidatePath } from "next/cache";

export async function updatePost(formData: FormData) {
await updatePostInDatabase(formData);

revalidatePath("/posts");
revalidatePath(`/posts/${formData.get("id")}`);
}

重新验证标签

"use server";

import { revalidateTag } from "next/cache";

export async function updateProducts() {
await updateProductsInDatabase();

revalidateTag("products");
}

重定向

"use server";

import { redirect } from "next/navigation";

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

redirect(`/posts/${post.id}`);
}
"use server";

import { cookies } from "next/headers";

export async function login(formData: FormData) {
const email = formData.get("email") as string;
const password = formData.get("password") as string;

const user = await verifyUser(email, password);

if (!user) {
return { error: "登录失败" };
}

const cookieStore = await cookies();
cookieStore.set("token", user.token, {
httpOnly: true,
secure: true,
sameSite: "strict",
maxAge: 60 * 60 * 24 * 7,
});

return { success: true };
}

export async function logout() {
const cookieStore = await cookies();
cookieStore.delete("token");
}

实战示例

完整的 CRUD 操作

// app/posts/actions.ts
"use server";

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { z } from "zod";
import { db } from "@/lib/db";

const postSchema = z.object({
title: z.string().min(3).max(100),
content: z.string().min(10),
});

export async function createPost(formData: FormData) {
const result = postSchema.safeParse({
title: formData.get("title"),
content: formData.get("content"),
});

if (!result.success) {
return { error: result.error.flatten().fieldErrors };
}

const post = await db.post.create({ data: result.data });
revalidatePath("/posts");
redirect(`/posts/${post.id}`);
}

export async function updatePost(id: string, formData: FormData) {
const result = postSchema.safeParse({
title: formData.get("title"),
content: formData.get("content"),
});

if (!result.success) {
return { error: result.error.flatten().fieldErrors };
}

await db.post.update({
where: { id },
data: result.data,
});

revalidatePath("/posts");
revalidatePath(`/posts/${id}`);
}

export async function deletePost(id: string) {
await db.post.delete({ where: { id } });
revalidatePath("/posts");
}
// app/posts/page.tsx
import { createPost } from "./actions";

export default function NewPostPage() {
return (
<form action={createPost}>
<div>
<label>标题</label>
<input name="title" type="text" required />
</div>
<div>
<label>内容</label>
<textarea name="content" required />
</div>
<button type="submit">发布</button>
</form>
);
}
// app/posts/[id]/page.tsx
import { deletePost } from "../actions";

export default function PostPage({ params }: { params: { id: string } }) {
return (
<div>
<h1>文章详情</h1>
<form action={() => deletePost(params.id)}>
<button type="submit">删除</button>
</form>
</div>
);
}

小结

本章我们学习了:

  1. Server Actions 的基本用法
  2. 表单处理和文件上传
  3. 状态处理(useFormState、useFormStatus)
  4. 数据验证
  5. 重新验证和重定向
  6. Cookie 操作

练习

  1. 创建一个带有验证的表单提交功能
  2. 实现一个完整的 CRUD 操作
  3. 使用 useFormState 处理表单错误
  4. 实现文件上传功能