Server Actions 是什么
async function createPost(formData: FormData) {
'use server'; // ★ 服务端函数
const title = formData.get('title');
await db.posts.create({ title });
}
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" required />
<button type="submit">Create</button>
</form>
);
}
提交表单 → createPost 在服务端跑 → 不用单独写 API endpoint。
标记方式
1. 函数内 directive
async function action(data: FormData) {
'use server';
// 这个函数在服务端跑
}
2. 文件顶部 directive
// src/actions/posts.ts
'use server';
export async function createPost(formData: FormData) { ... }
export async function deletePost(id: string) { ... }
文件里所有 export 函数都是 Server Action。
完整例子
// src/actions/posts.ts
'use server';
import { redirect } from 'next/navigation';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const body = formData.get('body') as string;
if (!title || !body) {
throw new Error('Missing fields');
}
const post = await db.posts.create({ title, body });
revalidatePath('/blog'); // 刷新列表
redirect(`/blog/${post.slug}`); // 跳转
}
// src/app/blog/new/page.tsx
import { createPost } from '@/actions/posts';
export default function NewPostPage() {
return (
<form action={createPost} className="space-y-4">
<input name="title" required className="border p-2 w-full" />
<textarea name="body" required className="border p-2 w-full h-32" />
<button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded">
Create
</button>
</form>
);
}
没有 fetch,没有 API endpoint,没有 useState——直接表单到数据库。
处理加载状态:useFormStatus
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create'}
</button>
);
}
// 用:
<form action={createPost}>
<input name="title" />
<SubmitButton />
</form>
useFormStatus 必须在 form 子组件里——所以 SubmitButton 单独提出。
处理返回值 / 错误:useActionState
'use client';
import { useActionState } from 'react';
import { createPost } from '@/actions/posts';
export default function NewPostForm() {
const [state, action, pending] = useActionState(createPost, null);
return (
<form action={action}>
<input name="title" />
{state?.error && <p className="text-red-600">{state.error}</p>}
<button type="submit" disabled={pending}>
{pending ? 'Saving...' : 'Save'}
</button>
</form>
);
}
// action 改成接 state 参数
'use server';
export async function createPost(prevState: any, formData: FormData) {
try {
// ...
return { ok: true };
} catch (err) {
return { error: err.message };
}
}
不用表单也能用 Server Action
'use client';
import { deletePost } from '@/actions/posts';
<button onClick={() => deletePost(post.id)}>Delete</button>
非表单调用就是普通函数——只是它跑在服务端。
鉴权(必须做)
Server Actions 可以被任何人调用——永远在 action 里验权限:
'use server';
import { auth } from '@/lib/auth';
export async function deletePost(id: string) {
const session = await auth();
if (!session) throw new Error('Unauthorized');
const post = await db.posts.findUnique({ where: { id } });
if (post.authorId !== session.userId) throw new Error('Forbidden');
await db.posts.delete({ where: { id } });
revalidatePath('/blog');
}
表单校验
'use server';
import { z } from 'zod';
const PostSchema = z.object({
title: z.string().min(1).max(200),
body: z.string().min(10),
});
export async function createPost(formData: FormData) {
const parsed = PostSchema.safeParse({
title: formData.get('title'),
body: formData.get('body'),
});
if (!parsed.success) {
return { errors: parsed.error.flatten() };
}
await db.posts.create({ data: parsed.data });
}
zod 是事实标准的 schema 校验库。
优势 vs 传统 API
| 维度 | Server Action | 传统 API endpoint |
|---|---|---|
| 文件数 | 1 个函数 | 1 个 API + 1 个组件 |
| 类型安全 | TS 自动跨边界 | 手动同步 |
| 网络请求 | 浏览器原生表单 / fetch(自动) | 手写 fetch |
| 学习曲线 | 低 | 中 |
何时用 API endpoint
- 给第三方调(webhook 接收方)
- 给浏览器之外的客户端(移动 App / curl)
- 流式响应
// src/app/api/posts/route.ts
export async function POST(request: Request) {
const data = await request.json();
// ...
return Response.json({ ok: true });
}
坑
- Server Actions 永远在服务端跑——
window/document不可用 - 大文件上传不要用 FormData——用 chunked upload / S3 直传
- Action 抛错会跳 error.tsx——要友好提示用 return error
- 别忘
revalidatePath/revalidateTag——否则缓存还是老数据
下一篇:SEO metadata。