服务器函数

2026年9月6日

Server Actions(也叫服务器函数)让 <form action={fn}> 直接打到一段在服务器跑的函数,不必先写 /api/login。函数收到原生 FormData,可以校验、写库、再 redirect


和手写 API 的差别

传统:页面 fetchroute.ts 解析 body → 数据库 → JSON → 前端再画结果。

Actions:提交表单 → 带 'use server' 的函数在服务器执行 → 直接碰数据库。少一层自己维护的 REST 形状,渐进增强也在:JS 没到时,表单仍能提交。

静态导出用不了这个,需要 Node 运行时。


基本写法

函数里必须有 'use server'。可以写在页面里,更常见是单独 actions.ts,文件顶上一行指令。

export default function Login() {
  async function handleLogin(formData: FormData) {
    "use server";
    const username = formData.get("username");
    const password = formData.get("password");
    const all = Object.fromEntries(formData);
    console.log(username, password, all);
  }

  return (
    <form action={handleLogin}>
      <input name="username" placeholder="用户名" />
      <input name="password" type="password" placeholder="密码" />
      <button type="submit">登录</button>
    </form>
  );
}
tsx

name 对得上才能从 FormData 取到。


额外参数:bind

表单字段之外还要带文章 id、当前用户一类「页面已知、输入框没有」的值,用 bind

async function handleLogin(id: number, formData: FormData) {
  "use server";
  console.log(id, Object.fromEntries(formData));
}

export default function Login() {
  const action = handleLogin.bind(null, 1);
  return <form action={action}>{/* ... */}</form>;
}
tsx

bind 的参数会出现在 FormData 前面。不要 bind 密钥或未校验的权限,客户端能看见序列化后的闭包。


zod + useActionState

校验放服务器,错误回给页面。useActionState 是客户端 hook。

"use server";

import { z } from "zod";

const schema = z.object({
  username: z.string().min(6, "用户名至少 6 位"),
  password: z.string().min(6, "密码至少 6 位"),
});

export async function handleLogin(
  _prev: { message: string },
  formData: FormData
) {
  const parsed = schema.safeParse(Object.fromEntries(formData));
  if (!parsed.success) {
    const msg = parsed.error.issues.map((i) => i.message).join("\n");
    return { message: msg };
  }
  return { message: "登录成功" };
}
ts
"use client";

import { useActionState } from "react";
import { handleLogin } from "./actions";

const initial = { message: "" };

export default function Login() {
  const [state, formAction, pending] = useActionState(handleLogin, initial);

  return (
    <form action={formAction}>
      {pending ? <p>提交中…</p> : null}
      {state.message ? <p>{state.message}</p> : null}
      <input name="username" />
      <input name="password" type="password" />
      <button type="submit" disabled={pending}>
        登录
      </button>
    </form>
  );
}
tsx

useActionState 三个返回值:当前 state、交给 form 的 action、是否进行中。第三个可选参数 permalink 给「JS 加载前的回跳 URL」用。

下一篇开始 SEO:先分清白帽 / 黑帽,再落到 Next 能配的那些文件。


参考文档