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}`);
}
Cookie 操作
"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>
);
}
小结
本章我们学习了:
- Server Actions 的基本用法
- 表单处理和文件上传
- 状态处理(useFormState、useFormStatus)
- 数据验证
- 重新验证和重定向
- Cookie 操作
练习
- 创建一个带有验证的表单提交功能
- 实现一个完整的 CRUD 操作
- 使用 useFormState 处理表单错误
- 实现文件上传功能