Actions
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';
export const server = {
myAction: defineAction({ /* ... */ })
}Actions 作为函数从 astro:actions 模块中导入。导入 actions 并在 UI 框架组件、表单 POST 请求 等客户端中使用,或在 Astro 组件中的 <script> 标签中调用它们。
当你调用一个 action 时,它会返回一个对象,其中 data 包含 JSON 序列化的结果,或 error 包含抛出的错误。
---
---
<script>
import { actions } from 'astro:actions';
async () => {
const { data, error } = await actions.myAction({ /* ... */ });
}
</script>编写你的第一个 action
按照下面的步骤定义一个 action 并在 Astro 页面的 script 标签中调用它。
创建一个
src/actions/index.ts文件并导出一个server对象。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()工具定义一个getGreetingaction。input属性将使用 Zod schema (EN) scheme 验证输入参数,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 `你好,${input.name}!` } }) }创建一个 Astro 组件,其中包含一个按钮,当点击时将使用
getGreetingaction 获取问候语。--- --- <button>获取问候语</button> <script> const button = document.querySelector('button'); button?.addEventListener('click', async () => { // 通过 action 弹出带有问候语的弹窗 }); </script>要使用 action,请从
astro:actions导入actions,然后在单击处理程序中调用actions.getGreeting()。name选项将被发送到服务器上的 action 的handler(),如果没有报错,你可以从data属性上获取到执行结果。--- --- <button>获取问候语</button> <script> import { actions } from 'astro:actions'; const button = document.querySelector('button'); button?.addEventListener('click', async () => { // 通过 action 弹出带有问候语的弹窗 const { data, error } = await actions.getGreeting({ name: "Houston" }); if (!error) alert(data); }) </script>
defineAction() 及其属性的详细信息,请参阅完整的 Actions API 文档。
组织 actions
在你的项目中,所有的 actions 都必须从 src/actions/index.ts 文件中的 server 对象中导出。你可以内联定义 actions,也可以将 action 定义移动到单独的文件中并导入它们。你甚至可以将相关函数分组到嵌套对象中。
例如,要将所有用户 actions 放在一起,你可以创建一个 src/actions/user.ts 文件,并将 getUser 和 createUser 的定义嵌套在一个 user 对象中。
import { defineAction } from 'astro:actions';
export const user = {
getUser: defineAction(/* ... */),
createUser: defineAction(/* ... */),
}然后,你可以将此 user 对象导入到你的 src/actions/index.ts 文件中,并将其作为顶层键与任何其他 actions 一起添加到 server 对象中:
import { user } from './user';
export const server = {
myAction: defineAction({ /* ... */ }),
user,
}现在,你可以从 actions.user 对象中调用所有用户 actions:
actions.user.getUser()actions.user.createUser()
处理返回的数据
Actions 会返回一个对象,这个对象要么是具备 handler() 类型安全性的返回值 data,要么是包含着任何后端错误的 error。错误可能来自 input 属性上的验证错误,或者来自 handler() 中抛出的错误。
Actions 可以返回 使用 Devalue 库 处理的自定义数据,如日期,Maps, Sets 和 URL。这意味着你不能像处理常规 JSON 一样轻松地检查网络响应。为了调试,你可以检查 actions 返回的 data 对象。
handler() API 参考。
检查错误
最好在使用 data 属性之前检查是否存在 error。这样可以提前处理错误,并确保 data 在没有 undefined 检查的情况下被定义。
const { data, error } = await actions.example();
if (error) {
// 处理错误
return;
}
// 使用 `data`直接访问 data 而不检查错误
为了跳过错误处理,例如在原型设计或使用将为你捕获错误的库时,可以在调用 action 时使用 .orThrow() 属性来抛出错误,而不是返回一个 error。这将直接返回 action 的 data。
这个例子调用了一个 likePost() action,它从 action handler 返回一个 number 类型的更新后的点赞数:
const updatedLikes = await actions.likePost.orThrow({ postId: 'example' });
// ^ type:number在 action 中处理后端错误
你可以使用提供的 ActionError 从你的 action handler() 中抛出错误,例如当数据库条目丢失时抛出“未找到”,或者当用户未登录时抛出“未经授权”。这比返回 undefined 有两个主要好处:
你可以设置一个状态码,例如
404 - 未找到或401 - 未经授权。这可以让你通过查看每个请求的状态码来改善开发和生产中错误调试的体验。在你的应用程序代码中,所有错误都会传递到 action 结果上的
error对象。这避免了对数据进行undefined检查的需要,并允许你根据出现的问题来向用户展示更针对性的反馈。
创建一个 ActionError
为了抛出一个错误,从 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";
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.",
});
}
// 否则,点赞成功
},
}),
};处理 ActionError
为了处理错误,你可以从你的应用程序中调用 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);
return (
<>
{
showLogin && <a href="/signin">登录后点赞文章。</a>
}
<button onClick={async () => {
const { data, error } = await actions.likePost({ postId });
if (error?.code === 'UNAUTHORIZED') setShowLogin(true);
// 因意外错误而提前终止
else if (error) return;
// 更新点赞数
}}>
点赞
</button>
</>
)
}处理客户端重定向
当在客户端调用 actions 时,你可以集成一个客户端库,例如 react-router,或者你可以使用 Astro 的 navigate() 函数 来在 action 成功时重定向到一个新页面。
这个例子在 logout action 成功返回后导航到主页:
import { actions } from 'astro:actions';
import { navigate } from 'astro:transitions/client';
export function LogoutButton() {
return (
<button onClick={async () => {
const { error } = await actions.logout();
if (!error) navigate('/');
}}>
退出登录
</button>
);
}从 action 接受表单数据
Actions 默认接受 JSON 数据。要接受来自 HTML 表单的表单数据,请在 defineAction() 调用中设置 accept: 'form':
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';
export const server = {
comment: defineAction({
accept: 'form',
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';
export const server = {
changeUser: defineAction({
accept: 'form',
input: z.discriminatedUnion('type', [
z.object({
// 当 `type` 字段的值为 `create` 时匹配
type: z.literal('create'),
name: z.string(),
email: z.email(),
}),
z.object({
// 当 `type` 字段的值为 `update` 时匹配
type: z.literal('update'),
id: z.number(),
name: z.string(),
email: z.email(),
}),
]),
async handler(input) {
if (input.type === 'create') {
// 输入为 { type: 'create', name: string, email: string }
} else {
// 输入为 { type: 'update', id: number, name: string, email: string }
}
},
}),
};校验表单数据
Actions 将解析提交的表单数据为一个对象,使用每个输入的 name 属性的值作为对象键。例如,包含 <input name="search"> 的表单将被解析为一个对象,如 { search: 'user input' }。你的 action 的 input 模式将用于验证此对象。
为了接收原始的 FormData 对象,而不是解析后的对象,可以在 action 定义中省略 input 属性。
下面的示例显示了一个验证过的 newsletter 注册表单,它接受用户的电子邮件并要求勾选“服务条款”复选框以同意。
创建一个 HTML 表单组件,为每个输入设置唯一的
name属性:<form> <label for="email">E-mail</label> <input id="email" required type="email" name="email" /> <label> <input required type="checkbox" name="terms"> 我已同意服务条款 </label> <button>注册</button> </form>定义一个
newsletteraction 来处理提交的表单。使用z.email()验证器验证email字段,使用z.boolean()验证器验证terms复选框:import { defineAction } from 'astro:actions'; import { z } from 'astro/zod'; export const server = { newsletter: defineAction({ accept: 'form', input: z.object({ email: z.email(), terms: z.boolean(), }), handler: async ({ email, terms }) => { /* ... */ }, }) }请参阅input验证器 查看所有可用的校验器添加一个
<script>到 HTML 表单中以提交用户输入。这个例子覆盖了表单的默认提交行为,调用actions.newsletter(),并使用navigate()函数重定向到/confirmation:<form> <label for="email">E-mail</label> <input id="email" required type="email" name="email" /> <label> <input required type="checkbox" name="terms"> 我已同意服务条款 </label> <button>注册</button> </form> <script> import { actions } from 'astro:actions'; import { navigate } from 'astro:transitions/client'; const form = document.querySelector('form'); form?.addEventListener('submit', async (event) => { event.preventDefault(); const formData = new FormData(form); const { error } = await actions.newsletter(formData); if (!error) navigate('/confirmation'); }) </script>请参阅 “从 HTML 表单操作调用 action” 以了解提交表单数据的另一种方法。
展示表单输入错误
你可以在提交前校验表单输入,使用原生 HTML 表单校验属性,例如 required、type="email" 和 pattern。对于后端的更复杂的 input 校验,你可以使用 isInputError() 工具函数。
要检索输入错误,可以使用 isInputError() 工具函数来检查错误是否是由无效输入而引起的。输入错误包含一个 fields 对象,其中包含每个验证失败的输入名称的消息。你可以使用这些消息提示用户以纠正他们的提交。
下面的示例使用 isInputError() 检查错误,然后检查错误是否在电子邮件字段中,最后从错误中创建消息。你可以使用 JavaScript DOM 操作或你喜欢的 UI 框架将此消息显示给用户。
import { actions, isInputError } from 'astro:actions';
const form = document.querySelector('form');
const formData = new FormData(form);
const { error } = await actions.newsletter(formData);
if (isInputError(error)) {
// 处理输入错误。
if (error.fields.email) {
const message = error.fields.email.join(', ');
}
}从 HTML 表单操作调用 action
:::note页面必须在使用表单操作调用 actions 时进行按需渲染。在使用此 API 之前,请确保页面上已禁用预渲染。:::
你可以在任何 <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}>
<button>退出登录</button>
</form>可能需要在 <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>
</form>在 action 成功时重定向
如果你需要在成功时重定向到一个新的路由,你可以在服务器上使用 action 的结果。一个常见的例子是创建一个产品记录并重定向到新产品的页面,例如 /products/[id]。
例如,假设你有一个 createProduct action,它返回生成的产品 id:
import { defineAction } from 'astro:actions';
import { z } from 'astro/zod';
export const server = {
createProduct: defineAction({
accept: 'form',
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}>
<!--...-->
</form>处理表单 action 错误
在 Astro 组件中调用 Astro.getActionResult(),可以让你访问 data 和 error 对象,以进行自定义错误处理。
下面的例子在 newsletter action 失败时显示一个通用的失败消息:
---
import { actions } from 'astro:actions';
const result = Astro.getActionResult(actions.newsletter);
---
{result?.error && (
<p class="error">无法注册。请稍后再试。</p>
)}
<form method="POST" action={actions.newsletter}>
<label>
E-mail
<input required type="email" name="email" />
</label>
<button>注册</button>
</form>为了更多的自定义,你可以使用 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}>
<label>
E-mail
<input required type="email" name="email" aria-describedby="error" />
</label>
{inputErrors.email && <p id="error">{inputErrors.email.join(',')}</p>}
<button>注册</button>
</form>在错误时保留输入值
输入框在提交表单时会被清空。为了保留输入值,你可以在页面上启用视图过渡,并对每个输入应用 transition:persist 指令:
<input transition:persist required type="email" name="email" />使用表单的 action 结果以更新 UI
要使用 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>
)}
<!--...-->高级:通过 session 持久化 action 结果
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();
// ... 处理 action 结果
setActionResult(action.name, serializeActionResult(result));
}
return next();
});保存 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, {
type: "json",
})
: undefined;
if (session) {
setActionResult(session.actionName, session.actionResult);
// 可选:页面渲染后删除 session。
// 随意实现你自己的持久化策略
await actionStore.delete(sessionId);
context.cookies.delete("action-session-id");
return next();
}
// 如果从 HTML 表单操作调用 action,
// 调用 action handler 并重定向到目标页面
if (action?.calledFrom === "form") {
const actionResult = await action.handler();
// 使用 session 存储保存 action 结果
const sessionId = randomUUID();
await actionStore.setJSON(sessionId, {
actionName: action.name,
actionResult: serializeActionResult(actionResult),
});
// 将 session ID 作为 cookie 传递
// 重定向到页面后检索
context.cookies.set("action-session-id", sessionId);
// 出错时重定向回上一页
if (actionResult.error) {
const referer = context.request.headers.get("Referer");
if (!referer) {
throw new Error(
"Internal: Referer unexpectedly missing from Action POST request.",
);
}
return context.redirect(referer);
}
// 成功则跳转到目标页面
return context.redirect(context.originPathname);
}
return next();
});使用 actions 时的安全性
可以根据 action 的名称作为公共端点访问 Action 。例如,action blog.like() 将可以从 /_actions/blog.like 访问。这对于单元测试 action 结果和调试生产错误非常有用。然而,这意味着你必须使用与 API 端点和按需渲染页面相同的授权检查。
从 action handler 授权用户
要授权 action 请求,请在 action handler 中添加身份验证检查。你可能希望使用 身份验证库 来处理会话管理和用户信息。
Action 公开 APIContext 对象的一个子集,以访问从中间件传递的属性,使用 context.locals。当用户未经授权时,你可以使用 UNAUTHORIZED 代码引发 ActionError:
import { defineAction, ActionError } from 'astro:actions';
export const server = {
getUserSettings: defineAction({
handler: async (_input, context) => {
if (!context.locals.user) {
throw new ActionError({ code: 'UNAUTHORIZED' });
}
return { /* data on success */ };
}
})
}中间件中的拦截器 actions
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 */);
return next();
});从 Astro 组件和服务器端点调用 action
你可以在 Astro 组件脚本中使用 Astro.callAction() 包装器直接调用 action(或者在使用 服务器端点时,使用 context.callAction())。这常用于在其他服务端代码中复用你的 actions 逻辑。
将 action 作为第一个参数传递,并将任何输入参数作为第二个参数传递。这将返回你在客户端上调用 action 时,所收到的相同的 data 和 error 对象:
---
import { actions } from 'astro:actions';
const searchQuery = Astro.url.searchParams.get('search');
if (searchQuery) {
const { data, error } = await Astro.callAction(actions.findProduct, { query: searchQuery });
// 处理结果
}
---
Learn