Actions
添加于:
astro@4.15
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:schema';
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按照下面的步骤定义一个 action 并在 Astro 页面的 script
标签中调用它。
-
创建一个
src/actions/index.ts
文件并导出一个server
对象。src/actions/index.ts export const server = {// action 声明} -
导入
astro:actions
中的defineAction()
工具以及astro:schema
中的z
对象。src/actions/index.ts import { defineAction } from 'astro:actions';import { z } from 'astro:schema';export const server = {// action 声明} -
使用
defineAction()
工具定义一个getGreeting
action。input
属性将使用 Zod scheme 验证输入参数,handler()
函数包含要在服务器上运行的后端逻辑。src/actions/index.ts import { defineAction } from 'astro:actions';import { z } from 'astro:schema';export const server = {getGreeting: defineAction({input: z.object({name: z.string(),}),handler: async (input) => {return `你好,${input.name}!`}})} -
创建一个 Astro 组件,其中包含一个按钮,当点击时将使用
getGreeting
action 获取问候语。src/pages/index.astro ------<button>获取问候语</button><script>const button = document.querySelector('button');button?.addEventListener('click', async () => {// 通过 action 弹出带有问候语的弹窗});</script> -
要使用 action,请从
astro:actions
导入actions
,然后在单击处理程序中调用actions.getGreeting()
。name
选项将被发送到服务器上的 action 的handler()
,如果没有报错,你可以从data
属性上获取到执行结果。src/pages/index.astro ------<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在你的项目中,所有的 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
而不检查错误
段落标题 直接访问 data 而不检查错误为了跳过错误处理,例如在原型设计或使用将为你捕获错误的库时,可以在调用 action 时使用 .orThrow()
属性来抛出错误,而不是返回一个 error
。这将直接返回 action 的 data
。
这个例子调用了一个 likePost()
action,它从 action handler
返回一个 number
类型的更新后的点赞数:
const updatedLikes = await actions.likePost.orThrow({ postId: 'example' });// ^ type:number
在 action 中处理后端错误
段落标题 在 action 中处理后端错误你可以使用提供的 ActionError
从你的 action handler()
中抛出错误,例如当数据库条目丢失时抛出“未找到”,或者当用户未登录时抛出“未经授权”。这比返回 undefined
有两个主要好处:
-
你可以设置一个状态码,例如
404 - 未找到
或401 - 未经授权
。这可以让你通过查看每个请求的状态码来改善开发和生产中错误调试的体验。 -
在你的应用程序代码中,所有错误都会传递到 action 结果上的
error
对象。这避免了对数据进行undefined
检查的需要,并允许你根据出现的问题来向用户展示更针对性的反馈。
创建一个 ActionError
段落标题 创建一个 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:schema";
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
段落标题 处理 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 接受表单数据
段落标题 从 action 接受表单数据Actions 默认接受 JSON 数据。要接受来自 HTML 表单的表单数据,请在 defineAction()
调用中设置 accept: 'form'
:
import { defineAction } from 'astro:actions';import { z } from 'astro:schema';
export const server = { comment: defineAction({ accept: 'form', input: z.object(/* ... */), handler: async (input) => { /* ... */ }, })}
校验表单数据
段落标题 校验表单数据Actions 将解析提交的表单数据为一个对象,使用每个输入的 name
属性的值作为对象键。例如,包含 <input name="search">
的表单将被解析为一个对象,如 { search: 'user input' }
。你的 action 的 input
模式将用于验证此对象。
为了接收原始的 FormData
对象,而不是解析后的对象,可以在 action 定义中省略 input
属性。
下面的示例显示了一个验证过的 newsletter 注册表单,它接受用户的电子邮件并要求勾选“服务条款”复选框以同意。
-
创建一个 HTML 表单组件,为每个输入设置唯一的
name
属性:src/components/Newsletter.astro <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> -
定义一个
newsletter
action 来处理提交的表单。使用z.string().email()
验证器验证email
字段,使用z.boolean()
验证器验证terms
复选框:src/actions/index.ts import { defineAction } from 'astro:actions';import { z } from 'astro:schema';export const server = {newsletter: defineAction({accept: 'form',input: z.object({email: z.string().email(),terms: z.boolean(),}),handler: async ({ email, terms }) => { /* ... */ },})}请参阅input
验证器 查看所有可用的校验器 -
添加一个
<script>
到 HTML 表单中以提交用户输入。这个例子覆盖了表单的默认提交行为,调用actions.newsletter()
,并使用navigate()
函数重定向到/confirmation
:src/components/Newsletter.astro <form>7 collapsed lines<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
段落标题 从 HTML 表单操作调用 action页面必须在使用表单操作调用 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>
在 action 成功时重定向
段落标题 在 action 成功时重定向如果你需要在成功时重定向到一个新的路由,你可以在服务器上使用 action 的结果。一个常见的例子是创建一个产品记录并重定向到新产品的页面,例如 /products/[id]
。
例如,假设你有一个 createProduct
action,它返回生成的产品 id:
import { defineAction } from 'astro:actions';import { z } from 'astro:schema';
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 错误
段落标题 处理表单 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 结果以更新 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 结果
段落标题 高级:通过 session 持久化 action 结果
添加于:
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(); // ... 处理 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 时的安全性
段落标题 使用 actions 时的安全性可以根据 action 的名称作为公共端点访问 Action 。例如,action blog.like()
将可以从 /_actions/blog.like
访问。这对于单元测试 action 结果和调试生产错误非常有用。然而,这意味着你必须使用与 API 端点和按需渲染页面相同的授权检查。
从 action handler 授权用户
段落标题 从 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
段落标题 中间件中的拦截器 actions
添加于:
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 */); return next();});
从 Astro 组件和服务器端点调用 action
段落标题 从 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 }); // 处理结果}---