操作
添加于: astro@4.15
Astro Actions 允许你以类型安全的方式定义和调用后端函数。Actions 会为你执行数据获取、JSON 解析和输入验证。与使用API 端点相比,这可以大大减少所需的样板代码。
使用 Actions 替代 API 端点,以实现客户端和服务器代码之间的无缝通信,并
- 使用 Zod 验证自动验证 JSON 和表单数据输入。
- 生成类型安全的函数,以便从客户端甚至从 HTML 表单的 action 中调用后端。无需手动调用
fetch()
。 - 使用
ActionError
对象来标准化后端错误。
基本用法
标题为“基本用法”的部分操作(Actions)在从 src/actions/index.ts
导出的 server
对象中定义。
import { defineAction } from 'astro:actions';import { z } from 'astro:schema';
export const server = { myAction: defineAction({ /* ... */ })}
你的操作可作为来自 astro:actions
模块的函数使用。导入 actions
并在UI 框架组件中、表单的 POST 请求中,或通过在 Astro 组件中使用 <script>
标签在客户端调用它们。
当你调用一个操作时,它会返回一个对象,其中包含带有 JSON 序列化结果的 data
,或包含抛出错误的 error
。
------
<script>import { actions } from 'astro:actions';
async () => { const { data, error } = await actions.myAction({ /* ... */ });}</script>
编写你的第一个操作
标题为“编写你的第一个操作”的部分请按照以下步骤定义一个操作,并在你的 Astro 页面的 script
标签中调用它。
-
创建一个
src/actions/index.ts
文件并导出一个server
对象。src/actions/index.ts export const server = {// action declarations} -
从
astro:actions
导入defineAction()
工具函数,并从astro:schema
导入z
对象。src/actions/index.ts import { defineAction } from 'astro:actions';import { z } from 'astro:schema';export const server = {// action declarations} -
使用
defineAction()
工具函数定义一个getGreeting
操作。input
属性将用于通过 Zod schema 验证输入参数,而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 `Hello, ${input.name}!`}})} -
创建一个带有按钮的 Astro 组件,点击按钮时将使用你的
getGreeting
操作来获取问候语。src/pages/index.astro ------<button>Get greeting</button><script>const button = document.querySelector('button');button?.addEventListener('click', async () => {// Show alert pop-up with greeting from action});</script> -
要使用你的操作,请从
astro:actions
导入actions
,然后在点击处理程序中调用actions.getGreeting()
。name
选项将被发送到服务器上操作的handler()
,如果没有错误,结果将作为data
属性可用。src/pages/index.astro ------<button>Get greeting</button><script>import { actions } from 'astro:actions';const button = document.querySelector('button');button?.addEventListener('click', async () => {// Show alert pop-up with greeting from actionconst { data, error } = await actions.getGreeting({ name: "Houston" });if (!error) alert(data);})</script>
defineAction()
及其属性的详细信息,请参阅完整的 Actions API 文档。
组织操作
标题为“组织操作”的部分你项目中的所有操作都必须从 src/actions/index.ts
文件中的 server
对象导出。你可以内联定义操作,也可以将操作定义移动到单独的文件中并导入它们。你甚至可以在嵌套对象中对相关函数进行分组。
例如,要将所有用户操作放在一起,你可以创建一个 src/actions/user.ts
文件,并将 getUser
和 createUser
的定义嵌套在单个 user
对象中。
import { defineAction } from 'astro:actions';
export const user = { getUser: defineAction(/* ... */), createUser: defineAction(/* ... */),}
然后,你可以将这个 user
对象导入到你的 src/actions/index.ts
文件中,并将其作为顶级键添加到 server
对象中,与任何其他操作并列。
import { user } from './user';
export const server = { myAction: defineAction({ /* ... */ }), user,}
现在,你所有的用户操作都可以从 actions.user
对象调用:
actions.user.getUser()
actions.user.createUser()
处理返回的数据
标题为“处理返回的数据”的部分操作返回一个对象,该对象包含带有 handler()
类型安全返回值的 data
,或带有任何后端错误的 error
。错误可能来自 input
属性的验证错误或在 handler()
中抛出的错误。
操作返回一种自定义数据格式,可以使用 Devalue 库处理日期、Map、Set 和 URL。因此,你无法像处理常规 JSON 那样轻松地检查来自网络的响应。为了调试,你可以检查操作返回的 data
对象。
handler()
API 参考。
检查错误
标题为“检查错误”的部分最好在使用 data
属性之前检查是否存在 error
。这使你可以提前处理错误,并确保 data
是已定义的,无需进行 undefined
检查。
const { data, error } = await actions.example();
if (error) { // handle error cases return;}// use `data`
不经错误检查直接访问 data
标题为“不经错误检查直接访问数据”的部分要跳过错误处理,例如在原型设计或使用会为你捕获错误的库时,请在你的操作调用上使用 .orThrow()
属性来抛出错误,而不是返回一个 error
。这将直接返回操作的 data
。
此示例调用一个 likePost()
操作,该操作从操作的 handler
中返回更新后的点赞数(一个 number
)。
const updatedLikes = await actions.likePost.orThrow({ postId: 'example' });// ^ type: number
在操作中处理后端错误
标题为“在操作中处理后端错误”的部分你可以使用提供的 ActionError
从你的操作 handler()
中抛出错误,例如当数据库条目缺失时抛出“not found”,或当用户未登录时抛出“unauthorized”。与返回 undefined
相比,这有两个主要好处:
-
你可以设置一个状态码,如
404 - Not found
或401 - Unauthorized
。这通过让你看到每个请求的状态码,改善了在开发和生产中调试错误的体验。 -
在你的应用程序代码中,所有错误都会传递给操作结果上的
error
对象。这避免了对数据进行undefined
检查的需要,并允许你根据出错的具体情况向用户显示有针对性的反馈。
创建一个 ActionError
标题为“创建一个 ActionError”的部分要抛出错误,请从 astro:actions
模块导入 ActionError()
类。向其传递一个人类可读的状态 code
(例如 "NOT_FOUND"
或 "BAD_REQUEST"
),以及一个可选的 message
以提供有关错误的更多信息。
本示例在检查一个假设的“user-session” cookie 进行身份验证后,如果用户未登录,则从 likePost
操作中抛出一个错误。
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.", }); } // Otherwise, like the post }, }),};
处理一个 ActionError
标题为“处理一个 ActionError”的部分要处理此错误,你可以从你的应用程序中调用该操作,并检查是否存在 error
属性。此属性的类型将是 ActionError
,并将包含你的 code
和 message
。
在以下示例中,一个 LikeButton.tsx
组件在被点击时调用 likePost()
操作。如果发生身份验证错误,将使用 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">Log in to like a post.</a> } <button onClick={async () => { const { data, error } = await actions.likePost({ postId }); if (error?.code === 'UNAUTHORIZED') setShowLogin(true); // Early return for unexpected errors else if (error) return; // update likes }}> Like </button> </> )}
处理客户端重定向
标题为“处理客户端重定向”的部分当从客户端调用操作时,你可以与像 react-router
这样的客户端库集成,或者当操作成功时,你可以使用 Astro 的 navigate()
函数重定向到一个新页面。
本示例在一个 logout
操作成功返回后导航到主页。
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('/'); }}> Logout </button> );}
从操作接受表单数据
标题为“从操作接受表单数据”的部分操作默认接受 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) => { /* ... */ }, })}
验证表单数据
标题为“验证表单数据”的部分操作会将提交的表单数据解析为一个对象,使用每个输入的 name
属性的值作为对象键。例如,一个包含 <input name="search">
的表单将被解析为一个类似 { search: 'user input' }
的对象。你的操作的 input
schema 将被用来验证这个对象。
要在你的操作处理程序中接收原始的 FormData
对象而不是解析后的对象,请在你的操作定义中省略 input
属性。
以下示例展示了一个经过验证的时事通讯注册表单,该表单接受用户的电子邮件并要求勾选“服务条款”协议复选框。
-
创建一个 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">I agree to the terms of service</label><button>Sign up</button></form> -
定义一个
newsletter
操作来处理提交的表单。使用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
API 参考。 -
向 HTML 表单添加一个
<script>
来提交用户输入。此示例覆盖了表单的默认提交行为,以调用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">I agree to the terms of service</label><button>Sign up</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()
检查错误,然后检查错误是否在 email 字段中,最后从错误中创建一条消息。你可以使用 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)) { // Handle input errors. if (error.fields.email) { const message = error.fields.email.join(', '); }}
从 HTML 表单的 action 调用操作
标题为“从 HTML 表单的 action 调用操作”的部分当使用表单的 action 调用操作时,页面必须是按需渲染的。在使用此 API 之前,请确保页面上的预渲染已禁用。
你可以通过在任何 <form>
元素上使用标准属性来启用零 JS 的表单提交。不带客户端 JavaScript 的表单提交既可以作为 JavaScript 加载失败时的后备方案,也可以在你偏好完全从服务器处理表单时使用。
在服务器上调用 Astro.getActionResult() 会返回你的表单提交结果(data
或 error
),并可用于动态重定向、处理表单错误、更新 UI 等。
要从 HTML 表单调用操作,请向你的 <form>
添加 method="POST"
,然后使用你的操作设置表单的 action
属性,例如 action={actions.logout}
。这会将 action
属性设置为使用一个由服务器自动处理的查询字符串。
例如,此 Astro 组件在按钮被点击时调用 logout
操作并重新加载当前页面。
---import { actions } from 'astro:actions';---
<form method="POST" action={actions.logout}> <button>Log out</button></form>
为了使用 Zod 进行正确的 schema 验证,<form>
元素上可能需要额外的属性。例如,要包含文件上传,请添加 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">Upload File</label> <input type="file" id="file" name="file" /> <button type="submit">Submit</button></form>
操作成功后重定向
标题为“操作成功后重定向”的部分如果你需要在成功时重定向到新路由,你可以在服务器上使用操作的结果。一个常见的例子是创建一个产品记录并重定向到新产品的页面,例如 /products/[id]
。
例如,假设你有一个 createProduct
操作,它返回生成的产品 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 组件中检索操作结果。当操作被调用时,它会返回一个包含 data
或 error
属性的对象;如果在此请求期间未调用操作,则返回 undefined
。
使用 data
属性来构建一个用于 Astro.redirect()
的 URL。
---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 组件中调用 Astro.getActionResult()
可以让你访问 data
和 error
对象以进行自定义错误处理。
以下示例在 newsletter
操作失败时显示一个通用的失败消息。
---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()
工具来检查错误是否由无效输入引起。
以下示例在提交了无效电子邮件时,在 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>Sign up</button></form>
出错时保留输入值
标题为“出错时保留输入值”的部分每当提交表单时,输入内容都将被清除。要持久化输入值,你可以启用视图过渡并将 transition:persist
指令应用于每个输入。
<input transition:persist required type="email" name="email" />
使用表单操作结果更新 UI
标题为“使用表单操作结果更新 UI”的部分要使用操作的返回值在成功时向用户显示通知,请将操作传递给 Astro.getActionResult()
。使用返回的 data
属性来渲染你想要显示的 UI。
此示例使用 addToCart
操作返回的 productName
属性来显示成功消息。
---import { actions } from 'astro:actions';
const result = Astro.getActionResult(actions.addToCart);---
{result && !result.error && ( <p class="success">Added {result.data.productName} to cart</p>)}
<!--...-->
高级:使用会话持久化操作结果
标题为“高级:使用会话持久化操作结果”的部分
添加于: astro@5.0.0
操作结果会作为 POST 提交来显示。这意味着当用户关闭并重新访问页面时,结果将被重置为 undefined
。如果用户尝试刷新页面,他们还会看到一个“确认重新提交表单?”的对话框。
要自定义此行为,你可以添加中间件来手动处理操作的结果。你可以选择使用 cookie 或会话存储来持久化操作结果。
首先创建一个中间件文件,并从 astro:actions
导入getActionContext()
工具。此函数返回一个 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(); // ... handle the action result setActionResult(action.name, serializeActionResult(result)); } return next();});
持久化 HTML 表单结果的常见做法是“POST / Redirect / GET”模式。此重定向消除了页面刷新时的“确认重新提交表单?”对话框,并允许在用户会话期间持久化操作结果。
此示例使用安装了 Netlify 服务器适配器的会话存储,将 POST / Redirect / GET 模式应用于所有表单提交。操作结果使用 Netlify Blob 写入会话存储,并在重定向后使用会话 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) => { // Skip requests for prerendered pages if (context.isPrerendered) return next();
const { action, setActionResult, serializeActionResult } = getActionContext(context); // Create a Blob store to persist action results with Netlify Blob const actionStore = getStore("action-session");
// If an action result was forwarded as a cookie, set the result // to be accessible from `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);
// Optional: delete the session after the page is rendered. // Feel free to implement your own persistence strategy await actionStore.delete(sessionId); context.cookies.delete("action-session-id"); return next(); }
// If an action was called from an HTML form action, // call the action handler and redirect to the destination page if (action?.calledFrom === "form") { const actionResult = await action.handler();
// Persist the action result using session storage const sessionId = randomUUID(); await actionStore.setJSON(sessionId, { actionName: action.name, actionResult: serializeActionResult(actionResult), });
// Pass the session ID as a cookie // to be retrieved after redirecting to the page context.cookies.set("action-session-id", sessionId);
// Redirect back to the previous page on error 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); } // Redirect to the destination page on success return context.redirect(context.originPathname); }
return next();});
使用操作时的安全性
标题为“使用操作时的安全性”的部分操作可以根据操作的名称作为公共端点访问。例如,操作 blog.like()
将可以从 /_actions/blog.like
访问。这对于单元测试操作结果和调试生产错误很有用。但是,这意味着你必须使用与 API 端点和按需渲染页面相同的授权检查。
从操作处理程序授权用户
标题为“从操作处理程序授权用户”的部分要授权操作请求,请在你的操作处理程序中添加身份验证检查。你可能需要使用身份验证库来处理会话管理和用户信息。
操作会暴露完整的 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 */ }; } })}
从中间件限制操作
标题为“从中间件限制操作”的部分
添加于: astro@5.0.0
Astro 建议从你的操作处理程序中授权用户会话,以尊重每个操作的权限级别和速率限制。但是,你也可以从中间件中限制对所有操作(或操作的子集)的请求。
使用你中间件中的 getActionContext()
函数来检索有关任何入站操作请求的信息。这包括操作名称以及该操作是使用客户端远程过程调用(RPC)函数(例如 actions.blog.like()
)还是 HTML 表单调用的。
以下示例拒绝所有没有有效会话令牌的操作请求。如果检查失败,将返回“Forbidden”响应。注意:此方法确保操作仅在存在会话时才可访问,但它不是安全授权的替代品。
import { defineMiddleware } from 'astro:middleware';import { getActionContext } from 'astro:actions';
export const onRequest = defineMiddleware(async (context, next) => { const { action } = getActionContext(context); // Check if the action was called from a client-side function if (action?.calledFrom === 'rpc') { // If so, check for a user session token if (!context.cookies.has('user-session')) { return new Response('Forbidden', { status: 403 }); } }
context.cookies.set('user-session', /* session token */); return next();});
从 Astro 组件和服务器端点调用操作
标题为“从 Astro 组件和服务器端点调用操作”的部分你可以使用 Astro.callAction()
包装器(或在使用服务器端点时使用 context.callAction()
)直接从 Astro 组件脚本中调用操作。这在其他服务器代码中重用你的操作逻辑时很常见。
将操作作为第一个参数传递,任何输入参数作为第二个参数。这将返回与在客户端调用操作时收到的相同的 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 }); // handle result}---