首页 / Astro 教程 / 动作(Actions):表单与 Server Actions

Astro 教程

动作(Actions):表单与 Server Actions

本教程共 56 篇 · 第 45 篇 · 更新于 2026-08-07 · 约 12 分钟阅读

AstroAstro 教程Actionsastro:actionsdefineActionServer Actions表单Zod

本节目标:学会用 astro:actions 定义后端函数(action),从客户端或 HTML 表单调用它,并完成输入校验与错误处理。

动作(Actions)是 Astro 在 astro@4.15 引入、并在 v7 中成熟的能力(本章以 v7.2.0 文档为准)。它让你用类型安全的方式定义并调用后端函数。相比自己手写 API 端点(第 34 章),Actions 帮你自动做了三件事:数据获取、JSON 解析、输入校验——样板代码少一大截。

Important

本章一律用 v7 的 astro:actions 写法。社区旧文常见的”自己写 <form>method="POST" 指向自定义端点、在端点里手动解析”思路,请丢弃——用 Actions 的 action 属性才是正路。Actions 内部也走表单提交,但校验和类型都由框架承包。

用 Actions 而不是端点的理由:

  • Zod 校验(Zod 是一个 schema 校验库,描述”数据应该长什么样”并自动检查)自动校验 JSON 和表单输入。
  • 生成类型安全的调用函数,从客户端甚至 HTML 表单里直接调,不用手写 fetch()
  • 用统一的 ActionError 对象规范后端错误。

基本用法:定义与调用

所有 action 都定义在一个从 src/actions/index.ts 导出的 server 对象里:

// src/actions/index.ts
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';

export const server = {
  myAction: defineAction({ /* ... */ })
}

astro:actions 模块导出 actions,你在客户端导入它就能像调普通函数一样调 action。调用后返回的对象里,要么是 data(JSON 序列化结果),要么是 error(抛出的错误):

---
---
<script>
import { actions } from 'astro:actions';

async () => {
  const { data, error } = await actions.myAction({ /* ... */ });
}
</script>

写第一个 action

跟着四步,定义一个能用的 action。

第一步,建 src/actions/index.ts,先导出空的 server 对象:

// src/actions/index.ts
export const server = {
  // action 声明写这里
}

第二步,从 astro:actions 导入 defineAction,从 astro/zod 导入 z

import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';

export const server = {
  // action 声明写这里
}

第三步,用 defineAction() 定义一个 getGreeting action。input 用 Zod schema 校验入参,handler() 是真正在服务器上跑的后端逻辑:

import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';

export const server = {
  getGreeting: defineAction({
    input: z.object({
      name: z.string(),
    }),
    handler: async (input) => {
      return `Hello, ${input.name}!`
    }
  })
}

第四步,在 Astro 页面里用 <script> 调用它。点按钮时 name 被发到服务器,没出错就拿到 data

---
---
<button>Get greeting</button>

<script>
import { actions } from 'astro:actions';

const button = document.querySelector('button');
button?.addEventListener('click', async () => {
  const { data, error } = await actions.getGreeting({ name: "Houston" });
  if (!error) alert(data);
})
</script>

组织 actions

所有 action 都必须从 src/actions/index.tsserver 对象导出。你可以直接写在里面,也可以把定义挪到单独文件再导入,还能用嵌套对象分组。

比如把用户相关 action 放到 src/actions/user.ts

// src/actions/user.ts
import { defineAction } from 'astro:actions';

export const user = {
  getUser: defineAction(/* ... */),
  createUser: defineAction(/* ... */),
}

再在 index.ts 里把它作为 server 的一个顶层键:

// src/actions/index.ts
import { user } from './user';

export const server = {
  myAction: defineAction({ /* ... */ }),
  user,
}

之后就能用 actions.user.getUser()actions.user.createUser() 调用。

处理返回值

action 返回的对象里:有 data(handler 的返回值,带类型)、或 error(后端错误,可能来自 input 校验失败,或 handler 里抛的错)。返回格式经过特殊编码,能处理 Date、Map、Set、URL 等类型,所以你不能直接像看普通 JSON 那样看网络响应——要调试就打印返回的 data 对象。

先检查 error 再取 data

稳妥写法是先判 error 是否存在:

const { data, error } = await actions.example();

if (error) {
  // 处理错误
  return;
}
// 这里放心用 data

跳过错误检查:.orThrow()

原型阶段或你已有别的库兜底时,可用 .orThrow() 直接拿 data,出错就抛异常:

const updatedLikes = await actions.likePost.orThrow({ postId: 'example' });
//    ^ 类型: number

后端错误:ActionError

ActionError 在 handler 里抛错,比如”记录不存在”返回 404、“未登录”返回 401。好处有二:带状态码方便排查;错误统一进 error 对象,不用对 dataundefined 检查。

// src/actions/index.ts
import { defineAction, ActionError } from "astro:actions";
import { z } from "astro/zod";

export const server = {
  likePost: defineAction({
    input: z.object({ postId: z.string() }),
    handler: async (input, ctx) => {
      if (!ctx.cookies.has('user-session')) {
        throw new ActionError({
          code: "UNAUTHORIZED",
          message: "User must be logged in.",
        });
      }
      // 否则点赞
    },
  }),
};

在调用处,用 error.code 决定怎么提示用户:

const { data, error } = await actions.likePost({ postId });
if (error?.code === 'UNAUTHORIZED') setShowLogin(true);

接收表单数据

action 默认收 JSON。要让它收 HTML 表单数据,在 defineAction() 里设 accept: 'form'

export const server = {
  comment: defineAction({
    accept: 'form',
    input: z.object(/* ... */),
    handler: async (input) => { /* ... */ },
  })
}

Astro 会对表单字段做特殊便利处理:number 输入框用 z.number() 校验;checkboxz.coerce.boolean()filez.instanceof(File);同名多个输入用 z.array(...);其余用 z.string()。空输入会转成 null(数组和布尔除外)。z.discriminatedUnion() 还能按某字段分流校验不同结构。

用 HTML 表单 action 调用(零 JS)

这是 Actions 最妙的一点:你可以完全不写客户端 JS,仅靠 <form> 的标准属性提交。提交后由 Astro.getActionResult() 在服务端拿到结果(dataerror),用来重定向、报错或更新 UI。

做法:给 <form>method="POST",把 action 属性设成你的 action(如 action={actions.logout})。Astro 会自动把它转成服务器处理的查询串:

---
import { actions } from 'astro:actions';
---
<form method="POST" action={actions.logout}>
  <button>Log out</button>
</form>

上传文件要加 enctype="multipart/form-data"

---
import { actions } from 'astro:actions';
---
<form method="POST" action={actions.upload} enctype="multipart/form-data" >
  <input type="file" id="file" name="file" />
  <button type="submit">Submit</button>
</form>

成功后重定向

假设有个 createProduct action 返回新商品 id,在服务端用 Astro.getActionResult() 拿到结果,再用 Astro.redirect() 跳到新页面:

---
import { actions } from 'astro:actions';

const result = Astro.getActionResult(actions.createProduct);
if (result && !result.error) {
  return Astro.redirect(`/products/${result.data.id}`);
}
---
<form method="POST" action={actions.createProduct}>
  <!-- ... -->
</form>

处理表单错误

Astro.getActionResult() 在表单所在组件里能拿到 dataerror。简单报错:

---
import { actions } from 'astro:actions';
const result = Astro.getActionResult(actions.newsletter);
---
{result?.error && (
  <p class="error">Unable to sign up. Please try again later.</p>
)}
<form method="POST" action={actions.newsletter}>
  <label>
    E-mail
    <input required type="email" name="email" />
  </label>
  <button>Sign up</button>
</form>

想要更细的字段级错误,用 isInputError() 工具判断是不是”输入校验失败”,再取 error.fields 里每个字段的信息:

---
import { actions, isInputError } from 'astro:actions';
const result = Astro.getActionResult(actions.newsletter);
const inputErrors = isInputError(result?.error) ? result.error.fields : {};
---
<form method="POST" action={actions.newsletter}>
  <label>
    E-mail
    <input required type="email" name="email" aria-describedby="error" />
  </label>
  {inputErrors.email && <p id="error">{inputErrors.email.join(',')}</p>}
  <button>Sign up</button>
</form>

安全:action 是公开端点

每个 action 都会按名字暴露成一个公开端点,比如 blog.like() 对应 /_actions/blog.like。这方便你单元测试和调试生产错误,但也意味着:你必须像对待 API 端点和按需渲染页面那样做鉴权

在 handler 里鉴权

在 handler 里检查登录态,未授权就抛 ActionError

export const server = {
  getUserSettings: defineAction({
    handler: async (_input, context) => {
      if (!context.locals.user) {
        throw new ActionError({ code: 'UNAUTHORIZED' });
      }
      return { /* 成功时的数据 */ };
    }
  })
};

用中间件统一拦截

v5+ 可在中间件里用 getActionContext() 拿到入站 action 请求的信息(action 名字、是从客户端 RPC 还是 HTML 表单调用),统一做权限闸门。例如拒绝”没有会话令牌”的客户端调用:

// src/middleware.ts
import { defineMiddleware } from "astro:middleware";
import { getActionContext } from "astro:actions";

export const onRequest = defineMiddleware(async (context, next) => {
  const { action } = getActionContext(context);
  if (action?.calledFrom === "rpc") {
    if (!context.cookies.has("user-session")) {
      return new Response("Forbidden", { status: 403 });
    }
  }
  return next();
});
Note

中间件拦截能保证”没有会话就访问不了 action”,但它不能替代细粒度授权。真正的权限分级仍应在每个 action 的 handler 里检查。

从组件脚本 / 端点调用

想在服务端复用 action 的逻辑(比如别的服务器代码里也要查数据),用 Astro.callAction() 包装:

---
import { actions } from 'astro:actions';
const searchQuery = Astro.url.searchParams.get('search');
if (searchQuery) {
  const { data, error } = await Astro.callAction(actions.findProduct, { query: searchQuery });
  // 处理结果
}
---

在服务端端点(endpoint)里则用 context.callAction(),返回一样是 dataerror

什么时候用 Actions,什么时候用端点

两者都能写后端逻辑。经验法则:凡是”客户端要调一个带输入、带校验的后端函数”——比如提交表单、点赞、增删数据——优先用 Actions,省去手写 fetch、手动解析和校验。需要更底层的响应控制(自定义状态码、流式返回、文件下载等),或要暴露成标准 REST 接口给外部调用,才用端点(第 34 章)。

一个常见疑问

“action 名字会暴露内部逻辑吗?” action 端点路径(如 /_actions/blog.like)确实可被看到,但这和任何 API 端点一样——关键不在隐藏名字,而在做好鉴权(本章”安全”一节)。不要把敏感判断只放在客户端,服务器侧必须再验一次。

小结

Actions 是 v7 推荐的”后端函数”写法:在 src/actions/index.tsserver 对象里用 defineAction() 定义,靠 Zod 的 input 校验、handler 写逻辑。客户端用 actions.xxx() 调用,拿到 { data, error };表单用 method="POST" action={actions.xxx} 实现零 JS 提交,服务端用 Astro.getActionResult() 读结果、ActionError / isInputError 处理错误。别忘了 action 是公开端点,务必做鉴权。这是比手写 API 端点更省事的现代方案。

到本章为止,集成、配置、CLI、TypeScript、开发工具栏、Actions 这条”工程化与后端”主线就讲完了。