动作(Actions):表单与 Server Actions
本教程共 56 篇 · 第 45 篇 · 更新于 2026-08-07 · 约 12 分钟阅读
本节目标:学会用
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.ts 的 server 对象导出。你可以直接写在里面,也可以把定义挪到单独文件再导入,还能用嵌套对象分组。
比如把用户相关 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 对象,不用对 data 做 undefined 检查。
// 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() 校验;checkbox 用 z.coerce.boolean();file 用 z.instanceof(File);同名多个输入用 z.array(...);其余用 z.string()。空输入会转成 null(数组和布尔除外)。z.discriminatedUnion() 还能按某字段分流校验不同结构。
用 HTML 表单 action 调用(零 JS)
这是 Actions 最妙的一点:你可以完全不写客户端 JS,仅靠 <form> 的标准属性提交。提交后由 Astro.getActionResult() 在服务端拿到结果(data 或 error),用来重定向、报错或更新 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() 在表单所在组件里能拿到 data 和 error。简单报错:
---
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(),返回一样是 data 和 error。
什么时候用 Actions,什么时候用端点
两者都能写后端逻辑。经验法则:凡是”客户端要调一个带输入、带校验的后端函数”——比如提交表单、点赞、增删数据——优先用 Actions,省去手写 fetch、手动解析和校验。需要更底层的响应控制(自定义状态码、流式返回、文件下载等),或要暴露成标准 REST 接口给外部调用,才用端点(第 34 章)。
一个常见疑问
“action 名字会暴露内部逻辑吗?” action 端点路径(如 /_actions/blog.like)确实可被看到,但这和任何 API 端点一样——关键不在隐藏名字,而在做好鉴权(本章”安全”一节)。不要把敏感判断只放在客户端,服务器侧必须再验一次。
小结
Actions 是 v7 推荐的”后端函数”写法:在 src/actions/index.ts 的 server 对象里用 defineAction() 定义,靠 Zod 的 input 校验、handler 写逻辑。客户端用 actions.xxx() 调用,拿到 { data, error };表单用 method="POST" action={actions.xxx} 实现零 JS 提交,服务端用 Astro.getActionResult() 读结果、ActionError / isInputError 处理错误。别忘了 action 是公开端点,务必做鉴权。这是比手写 API 端点更省事的现代方案。
到本章为止,集成、配置、CLI、TypeScript、开发工具栏、Actions 这条”工程化与后端”主线就讲完了。