Astro Actions 允许你定义和调用具有类型安全性的后端函数。Actions 为你执行数据请求、JSON 解析和输入验证。与使用
API 端点
相比,这可以大大减少所需的样板代码量。
使用 actions 而不是 API 端点,以实现客户端和服务器代码之间的无缝通信,并且可以:
使用
Zod
自动校验 JSON 和表单数据输入。
生成类型安全的函数,以便从客户端调用后端,甚至可以
从 HTML 表单操作
中调用。无需手动
fetch()
调用。
使用
ActionError
对象标准化后端错误。
Actions 是在
src/actions/index.ts
中导出的
server
对象中定义的:
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';
myAction: defineAction({ /* ... */ })
Actions 作为函数从
astro:actions
模块中导入。导入
actions
并在
UI 框架组件
、
表单 POST 请求
等客户端中使用,或在 Astro 组件中的
<script>
标签中调用它们。
当你调用一个 action 时,它会返回一个对象,其中
data
包含 JSON 序列化的结果,或
error
包含抛出的错误。
import { actions } from 'astro:actions';
const { data, error } = await actions.myAction({ /* ... */ });
按照下面的步骤定义一个 action 并在 Astro 页面的
script
标签中调用它。
创建一个
src/actions/index.ts
文件并导出一个
server
对象。
导入
astro:actions
中的
defineAction()
工具以及
astro/zod
中的
z
对象。
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';
使用
defineAction()
工具定义一个
getGreeting
action。
input
属性将使用
Zod schema (EN)
scheme 验证输入参数,
handler()
函数包含要在服务器上运行的后端逻辑。
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';
getGreeting: defineAction({
handler: async (input) => {
return `你好,${input.name}!`
创建一个 Astro 组件,其中包含一个按钮,当点击时将使用
getGreeting
action 获取问候语。
const button = document.querySelector('button');
button?.addEventListener(
'click', async () => {
要使用 action,请从
astro:actions
导入
actions
,然后在单击处理程序中调用
actions.getGreeting()
。
name
选项将被发送到服务器上的 action 的
handler()
,如果没有报错,你可以从
data
属性上获取到执行结果。
import { actions } from 'astro:actions';
const button = document.querySelector('button');
button?.addEventListener('click', async () => {
const { data, error } = await actions.getGreeting({ name: "Houston" });
在你的项目中,所有的 actions 都必须从
src/actions/index.ts
文件中的
server
对象中导出。你可以内联定义 actions,也可以将 action 定义移动到单独的文件中并导入它们。你甚至可以将相关函数分组到嵌套对象中。
例如,要将所有用户 actions 放在一起,你可以创建一个
src/actions/user.ts
文件,并将
getUser
和
createUser
的定义嵌套在一个
user
对象中。
import { defineAction } from 'astro:actions';
getUser: defineAction(/* ... */),
createUser: defineAction(/* ... */),
然后,你可以将此
user
对象导入到你的
src/actions/index.ts
文件中,并将其作为顶层键与任何其他 actions 一起添加到
server
对象中:
import { user } from './user';
myAction: defineAction({ /* ... */ }),
现在,你可以从
actions.user
对象中调用所有用户 actions:
actions.user.getUser()
actions.user.createUser()
Actions 会返回一个对象,这个对象要么是具备
handler()
类型安全性的返回值
data
,要么是包含着任何后端错误的
error
。错误可能来自
input
属性上的验证错误,或者来自
handler()
中抛出的错误。
Actions 可以返回
使用 Devalue 库
处理的自定义数据,如日期,Maps, Sets 和 URL。这意味着你不能像处理常规 JSON 一样轻松地检查网络响应。为了调试,你可以检查 actions 返回的
data
对象。
最好在使用
data
属性之前检查是否存在
error
。这样可以提前处理错误,并确保
data
在没有
undefined
检查的情况下被定义。
为了跳过错误处理,例如在原型设计或使用将为你捕获错误的库时,可以在调用 action 时使用
.orThrow()
属性来抛出错误,而不是返回一个
error
。这将直接返回 action 的
data
。
这个例子调用了一个
likePost()
action,它从 action
handler
返回一个
number
类型的更新后的点赞数:
你可以使用提供的
ActionError
从你的 action
handler()
中抛出错误,例如当数据库条目丢失时抛出“未找到”,或者当用户未登录时抛出“未经授权”。这比返回
undefined
有两个主要好处:
你可以设置一个状态码,例如
404 - 未找到
或
401 - 未经授权
。这可以让你通过查看每个请求的状态码来改善开发和生产中错误调试的体验。
在你的应用程序代码中,所有错误都会传递到 action 结果上的
error
对象。这避免了对数据进行
undefined
检查的需要,并允许你根据出现的问题来向用户展示更针对性的反馈。
为了抛出一个错误,从
astro:actions
模块中导入
ActionError()
类
。传递一个人类可读的状态
code
(例如
"NOT_FOUND"
或
"BAD_REQUEST"
),以及一个可选的
message
以提供有关错误的更多信息。
当用户并未登录,且在检查了假设的 “use-session” cookie 进行身份验证后,这个例子从
likePost
action 抛出一个错误:
import { defineAction, ActionError } from "astro:actions";
import { z } from "astro/zod";
input: z.object({ postId: z.string() }),
handler: async (input, ctx) => {
if (!ctx.cookies.has('user-session')) {
message: "User must be logged in.",
为了处理错误,你可以从你的应用程序中调用 action 并检查是否存在
error
属性。这个属性将是
ActionError
类型,并将包含你的
code
和
message
。
在下面的例子中,一个
LikeButton.tsx
组件在点击时调用
likePost()
action。如果发生身份验证错误,
error.code
属性用于确定是否显示登录链接:
import { actions } from 'astro:actions';
import { useState } from 'preact/hooks';
export function LikeButton({ postId }: { postId: string }) {
const [showLogin, setShowLogin] = useState(false);
showLogin && <a href="/signin">登录后点赞文章。</a>
<button onClick={async () => {
const { data, error } = await actions.likePost({ postId });
if (error?.code ===
'UNAUTHORIZED') setShowLogin(true);
当在客户端调用 actions 时,你可以集成一个客户端库,例如
react-router
,或者你可以使用 Astro 的
navigate()
函数
来在 action 成功时重定向到一个新页面。
这个例子在
logout
action 成功返回后导航到主页:
import { actions } from 'astro:actions';
import { navigate } from 'astro:transitions/client';
export function LogoutButton() {
<button onClick={async () => {
const { error } = await actions.logout();
if (!error) navigate('/');
Actions 默认接受 JSON 数据。要接受来自 HTML 表单的表单数据,请在
defineAction()
调用中设置
accept: 'form'
:
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';
input: z.object(/* ... */),
handler: async (input) => { /* ... */ },
当你的 action
配置为接受表单数据
时,你可以使用任何 Zod 验证器来验证字段(例如用于日期输入的
z.coerce.date()
)。在
z.object()
验证器上也支持扩展函数,包括
.refine()
、
.transform()
和
.pipe()
。
此外,Astro 在底层为你提供了特殊处理,以便方便地验证以下类型的字段输入:
使用
z.number()
验证类型为
number
的输入
使用
z.coerce.boolean()
验证类型为
checkbox
的输入
使用
z.instanceof(File)
验证类型为
file
的输入
使用
z.array(/* validator */)
验证相同
name
的多个输入
使用
z.string()
验证所有其他输入
当你的表单提交时若包含空输入,输出类型可能与你的
input
校验器不匹配。空值会被转换为
null
,但在验证数组或布尔值时除外。例如,如果一个类型为
text
的输入以空值提交,结果将是
null
,而不是空字符串(
""
)。
要应用不同验证器的并集,请使用
z.discriminatedUnion()
包装器,根据特定表单字段缩小类型。此示例接受 “create” 或 “update” 用户的表单提交,使用名称为
type
的表单字段来确定应针对哪个对象进行验证:
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';
changeUser: defineAction({
input: z.discriminatedUnion('type', [
// 当 `type` 字段的值为 `create` 时匹配
type: z.literal('create'),
// 当 `type` 字段的值为 `update` 时匹配
type: z.literal('update'),
if (input.type === 'create') {
// 输入为 { type: 'create', name: string, email: string }
// 输入为 { type: 'update', id: number, name: string, email: string }
Actions 将解析提交的表单数据为一个对象,使用每个输入的
name
属性的值作为对象键。例如,包含
<input name="search">
的表单将被解析为一个对象,如
{ search: 'user input' }
。你的 action 的
input
模式将用于验证此对象。
为了接收原始的
FormData
对象,而不是解析后的对象,可以在 action 定义中省略
input
属性。
下面的示例显示了一个验证过的 newsletter 注册表单,它接受用户的电子邮件并要求勾选“服务条款”复选框以同意。
创建一个 HTML 表单组件,为每个输入设置唯一的
name
属性:
<label for="email">E-mail</label>
<input id="email" required type="email" name="email" />
<input required type="checkbox" name="terms">
定义一个
newsletter
action 来处理提交的表单。使用
z.email()
验证器验证
email
字段,使用
z.boolean()
验证器验证
terms
复选框:
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';
newsletter: defineAction({
handler: async ({ email, terms }) => { /* ... */ },
添加一个
<script>
到 HTML 表单中以提交用户输入。这个例子覆盖了表单的默认提交行为,调用
actions.newsletter()
,并使用
navigate()
函数重定向到
/confirmation
:
<label for="email">E-mail</label>
<input id="email" required type="email" name="email" />
<input required type="checkbox" name="terms">
import { actions } from 'astro:actions';
import { navigate } from 'astro:transitions/client';
const form = document.querySelector('form');
form?.addEventListener('submit', async (event) => {
const formData = new FormData(form);
const { error } = await actions.newsletter(formData);
if (!error) navigate('/confirmation');
你可以在提交前校验表单输入,使用原生 HTML 表单校验属性,例如
required
、
type="email"
和
pattern
。对于后端的更复杂的
input
校验,你可以使用
isInputError()
工具函数。
要检索输入错误,可以使用
isInputError()
工具函数来检查错误是否是由无效输入而引起的。输入错误包含一个
fields
对象,其中包含每个验证失败的输入名称的消息。你可以使用这些消息提示用户以纠正他们的提交。
下面的示例使用
isInputError()
检查错误,然后检查错误是否在电子邮件字段中,最后从错误中创建消息。你可以使用 JavaScript DOM 操作或你喜欢的 UI 框架将此消息显示给用户。
你可以在任何
<form>
元素上使用标准属性启用零 JS 表单提交。无论是作为 JavaScript 加载失败时的回退,又或者你更喜欢仅从服务器来处理表单时,无需客户端 JavaScript 的表单提交都非常有用。
在服务器上调用
Astro.getActionResult()
会返回表单提交的结果(
data
或
error
),并且可以用于动态重定向、处理表单错误、更新 UI 等。
要从 HTML 表单调用 action,可以为
<form>
添加
method="POST"
,并设置表单的
action
属性使用你的 action,例如
action={actions.logout}
。这会将
action
属性设置为使用服务器自动处理的查询字符串。
例如,这个 Astro 组件在点击按钮时调用
logout
action 并重新加载当前页面:
import { actions } from 'astro:actions';
<form method="POST" action={actions.logout}>
可能需要在
<form>
元素上添加额外的属性以实现 Zod 模式(Schema)的正确验证。例如,当包含文件上传功能时,必须添加
enctype="multipart/form-data"
属性,以保证文件数据以能被 z.instanceof(File) 正确识别的格式传输。
import { actions } from 'astro:actions';
<form method="POST" action={actions.upload} enctype="multipart/form-data" >
<label for="file">上传文件</label>
<input type="file" id="file" name="file" />
<button type="submit">提交</button>
如果你需要在成功时重定向到一个新的路由,你可以在服务器上使用 action 的结果。一个常见的例子是创建一个产品记录并重定向到新产品的页面,例如
/products/[id]
。
例如,假设你有一个
createProduct
action,它返回生成的产品 id:
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';
createProduct: defineAction({
input: z.object({ /* ... */ }),
handler: async (input) => {
const product = await persistToDatabase(input);
return { id: product.id };
你可以通过调用
Astro.getActionResult()
从你的 Astro 组件中检索 action 结果。当调用 action 时,它返回一个包含
data
或
error
属性的对象,或者如果在此请求期间未调用 action,则返回
undefined
。
使用
data
属性构建一个 URL,然后使用
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}>
在 Astro 组件中调用
Astro.getActionResult()
,可以让你访问
data
和
error
对象,以进行自定义错误处理。
下面的例子在
newsletter
action 失败时显示一个通用的失败消息:
import { actions } from 'astro:actions';
const result = Astro.getActionResult(actions.newsletter);
<p class="error">无法注册。请稍后再试。</p>
<form method="POST" action={actions.newsletter}>
<input required type="email" name="email" />
为了更多的自定义,你可以
使用
isInputError()
工具
来检查错误是否由无效输入引起。
下面的例子在提交无效的电子邮件时,在
email
输入字段下方呈现一个错误横幅:
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}>
<input required type="email" name="email" aria-describedby="error" />
{inputErrors.email && <p id="error">{inputErrors.email.join(',')}</p>}
输入框在提交表单时会被清空。为了保留输入值,你可以
在页面上启用视图过渡
,并对每个输入应用
transition:persist
指令:
要使用 action 的返回值在成功时向用户显示通知,将 action 传递给
Astro.getActionResult()
。使用返回的
data
属性来渲染你想要显示的 UI。
这个例子使用
addToCart
action 返回的
productName
属性来显示一个成功消息:
import { actions } from 'astro:actions';
const result = Astro.getActionResult(actions.addToCart);
{result && !result.error && (
<p class="success">添加 {result.data.productName} 到购物车</p>
astro@5.0.0
Action 的结果显示为 POST 提交。这意味着当用户关闭并重新访问页面时,结果将重置为
undefined
。如果用户尝试刷新页面,他们还将看到一个 “确认重新提交表单?” 对话框。
要自定义此行为,你可以添加中间件来手动处理 action 结果。你可以选择使用 cookie 或会话存储来持久化 action 结果。
首先
创建一个中间件文件
,并从
astro:actions
导入
getActionContext()
工具
。这个函数返回一个
action
对象,其中包含有关传入 action 请求的信息,包括 action 处理程序以及 action 是否是从 HTML 表单调用的。
getActionContext()
还返回
setActionResult()
和
serializeActionResult()
函数,用于以编程方式设置
Astro.getActionResult()
返回的值:
import { defineMiddleware } from 'astro:middleware';
import { getActionContext } from 'astro:actions';
export const onRequest = defineMiddleware(async (context, next) => {
const { action, setActionResult, serializeActionResult } = getActionContext(context);
if (action?.calledFrom === 'form') {
const result = await action.handler();
setActionResult(action.name, serializeActionResult(result));
保存 HTML 表单结果的常见做法是
POST / Redirect / GET 模式
。这个重定向会在页面刷新时移除 “确认重新提交表单?” 对话框,并允许 action 结果在用户会话期间持久化。
此示例使用安装了
Netlify 服务器适配器
的 session 存储,将 POST / 重定向 / GET 模式应用于所有表单提交。使用
Netlify Blob
将 action 结果写入 session 存储,并在重定向后使用 session ID 检索:
import { defineMiddleware } from 'astro:middleware';
import { getActionContext } from 'astro:actions';
import { randomUUID } from "node:crypto";
import { getStore } from "@netlify/blobs";
export const onRequest = defineMiddleware(async (context, next) => {
if (context.isPrerendered) return next();
const { action, setActionResult, serializeActionResult } =
getActionContext(context);
// 创建 Blob 存储以使用 Netlify Blob 保存 action 结果
const actionStore = getStore("action-session");
// 如果 action 结果作为 cookie 转发,则将 result
// 设置为可从 `Astro.getActionResult()` 访问
const sessionId = context.cookies.get("action-session-id")?.value;
const session = sessionId
? await actionStore.get(sessionId, {
setActionResult(session.actionName, session.actionResult);
await actionStore.delete(sessionId);
context.cookies.delete("action-session-id");
// 如果从 HTML 表单操作调用 action,
// 调用 action handler 并重定向到目标页面
if (action?.calledFrom === "form") {
const actionResult = await action.handler();
// 使用 session 存储保存 action 结果
const sessionId = randomUUID();
await actionStore.setJSON(sessionId, {
actionResult: serializeActionResult(actionResult),
// 将 session ID 作为 cookie 传递
context.cookies.set("action-session-id", sessionId);
if (actionResult.error) {
const referer = context.request.headers.get("Referer");
"Internal: Referer unexpectedly missing from Action POST request.",
return context
.redirect(referer);
return context.redirect(context.originPathname);
可以根据 action 的名称作为公共端点访问 Action 。例如,action
blog.like()
将可以从
/_actions/blog.like
访问。这对于单元测试 action 结果和调试生产错误非常有用。然而,这意味着你
必须
使用与 API 端点和按需渲染页面相同的授权检查。
要授权 action 请求,请在 action handler 中添加身份验证检查。你可能希望使用
身份验证库
来处理会话管理和用户信息。
Action 公开
APIContext
对象的一个子集
,以访问从中间件传递的属性,使用
context.locals
。当用户未经授权时,你可以使用
UNAUTHORIZED
代码引发
ActionError
:
import { defineAction, ActionError } from 'astro:actions';
getUserSettings: defineAction({
handler: async (_input, context) => {
if (!context.locals.user) {
throw new ActionError({ code: 'UNAUTHORIZED' });
return { /* data on success */ };
astro@5.0.0
Astro 建议从 action handler 中授权用户会话,以遵从每个 action 的权限级别和速率限制。但是,你也可以从中间件中拦截所有 action 请求(或一组 action 请求)。
使用
getActionContext()
函数
从你的中间件中检索有关任何传入 action 请求的信息。这包括 action 名称以及该 action 是否是使用客户端远程过程调用(RPC)函数(例如
actions.blog.like()
)或 HTML 表单调用的。
以下示例拒绝所有没有有效会话令牌的 action 请求。如果检查失败,将返回一个 “Forbidden” 响应。注意:此方法确保只有在会话存在时才能访问 action,但不是安全授权的替代品。
import { defineMiddleware } from 'astro:middleware';
import { getActionContext } from 'astro:actions';
export const onRequest = defineMiddleware(async (context, next) => {
const { action } = getActionContext(context);
// 检查该 action 是否是从客户端函数调用的
if (action?.calledFrom === 'rpc') {
// 如果是,检查用户 session token
if (!context.cookies.has('user-session')) {
return new Response('Forbidden', { status: 403 });
context.cookies.set('user-session', /* session token */);
你可以在 Astro 组件脚本中使用
Astro.callAction()
包装器直接调用 action(或者在使用
服务器端点
时,使用
context.callAction()
)。这常用于在其他服务端代码中复用你的 actions 逻辑。
将 action 作为第一个参数传递,并将任何输入参数作为第二个参数传递。这将返回你在客户端上调用 action 时,所收到的相同的
data
和
error
对象:
import { actions } from 'astro:actions';
const searchQuery = Astro.url.searchParams.get('search');
const { data, error } = await Astro.callAction(actions.findProduct, { query: searchQuery });