跳转到内容

操作

添加于: astro@4.15

Astro Actions 允许你以类型安全的方式定义和调用后端函数。Actions 会为你执行数据获取、JSON 解析和输入验证。与使用API 端点相比,这可以大大减少所需的样板代码。

使用 Actions 替代 API 端点,以实现客户端和服务器代码之间的无缝通信,并

  • 使用 Zod 验证自动验证 JSON 和表单数据输入。
  • 生成类型安全的函数,以便从客户端甚至从 HTML 表单的 action 中调用后端。无需手动调用 fetch()
  • 使用 ActionError 对象来标准化后端错误。

操作(Actions)在从 src/actions/index.ts 导出的 server 对象中定义。

src/actions/index.ts
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

src/pages/index.astro
---
---
<script>
import { actions } from 'astro:actions';
async () => {
const { data, error } = await actions.myAction({ /* ... */ });
}
</script>

请按照以下步骤定义一个操作,并在你的 Astro 页面的 script 标签中调用它。

  1. 创建一个 src/actions/index.ts 文件并导出一个 server 对象。

    src/actions/index.ts
    export const server = {
    // action declarations
    }
  2. 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
    }
  3. 使用 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}!`
    }
    })
    }
  4. 创建一个带有按钮的 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>
  5. 要使用你的操作,请从 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 action
    const { data, error } = await actions.getGreeting({ name: "Houston" });
    if (!error) alert(data);
    })
    </script>
有关 defineAction() 及其属性的详细信息,请参阅完整的 Actions API 文档。

你项目中的所有操作都必须从 src/actions/index.ts 文件中的 server 对象导出。你可以内联定义操作,也可以将操作定义移动到单独的文件中并导入它们。你甚至可以在嵌套对象中对相关函数进行分组。

例如,要将所有用户操作放在一起,你可以创建一个 src/actions/user.ts 文件,并将 getUsercreateUser 的定义嵌套在单个 user 对象中。

src/actions/user.ts
import { defineAction } from 'astro:actions';
export const user = {
getUser: defineAction(/* ... */),
createUser: defineAction(/* ... */),
}

然后,你可以将这个 user 对象导入到你的 src/actions/index.ts 文件中,并将其作为顶级键添加到 server 对象中,与任何其他操作并列。

src/actions/index.ts
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`

要跳过错误处理,例如在原型设计或使用会为你捕获错误的库时,请在你的操作调用上使用 .orThrow() 属性来抛出错误,而不是返回一个 error。这将直接返回操作的 data

此示例调用一个 likePost() 操作,该操作从操作的 handler 中返回更新后的点赞数(一个 number)。

const updatedLikes = await actions.likePost.orThrow({ postId: 'example' });
// ^ type: number

你可以使用提供的 ActionError 从你的操作 handler() 中抛出错误,例如当数据库条目缺失时抛出“not found”,或当用户未登录时抛出“unauthorized”。与返回 undefined 相比,这有两个主要好处:

  • 你可以设置一个状态码,如 404 - Not found401 - Unauthorized。这通过让你看到每个请求的状态码,改善了在开发和生产中调试错误的体验。

  • 在你的应用程序代码中,所有错误都会传递给操作结果上的 error 对象。这避免了对数据进行 undefined 检查的需要,并允许你根据出错的具体情况向用户显示有针对性的反馈。

要抛出错误,请从 astro:actions 模块导入 ActionError() 类。向其传递一个人类可读的状态 code(例如 "NOT_FOUND""BAD_REQUEST"),以及一个可选的 message 以提供有关错误的更多信息。

本示例在检查一个假设的“user-session” cookie 进行身份验证后,如果用户未登录,则从 likePost 操作中抛出一个错误。

src/actions/index.ts
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
},
}),
};

要处理此错误,你可以从你的应用程序中调用该操作,并检查是否存在 error 属性。此属性的类型将是 ActionError,并将包含你的 codemessage

在以下示例中,一个 LikeButton.tsx 组件在被点击时调用 likePost() 操作。如果发生身份验证错误,将使用 error.code 属性来确定是否显示登录链接。

src/components/LikeButton.tsx
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 操作成功返回后导航到主页。

src/pages/LogoutButton.tsx
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'

src/actions/index.ts
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 属性。

以下示例展示了一个经过验证的时事通讯注册表单,该表单接受用户的电子邮件并要求勾选“服务条款”协议复选框。

  1. 创建一个 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>
  2. 定义一个 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 参考
  3. 向 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 表单验证属性(如 requiredtype="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(', ');
}
}

你可以通过在任何 <form> 元素上使用标准属性来启用零 JS 的表单提交。不带客户端 JavaScript 的表单提交既可以作为 JavaScript 加载失败时的后备方案,也可以在你偏好完全从服务器处理表单时使用。

在服务器上调用 Astro.getActionResult() 会返回你的表单提交结果(dataerror),并可用于动态重定向、处理表单错误、更新 UI 等。

要从 HTML 表单调用操作,请向你的 <form> 添加 method="POST",然后使用你的操作设置表单的 action 属性,例如 action={actions.logout}。这会将 action 属性设置为使用一个由服务器自动处理的查询字符串。

例如,此 Astro 组件在按钮被点击时调用 logout 操作并重新加载当前页面。

src/components/LogoutButton.astro
---
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) 正确识别的格式发送。

src/components/FileUploadForm.astro
---
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。

src/actions/index.ts
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 组件中检索操作结果。当操作被调用时,它会返回一个包含 dataerror 属性的对象;如果在此请求期间未调用操作,则返回 undefined

使用 data 属性来构建一个用于 Astro.redirect() 的 URL。

src/pages/products/create.astro
---
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() 可以让你访问 dataerror 对象以进行自定义错误处理。

以下示例在 newsletter 操作失败时显示一个通用的失败消息。

src/pages/index.astro
---
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 输入字段下渲染一个错误横幅。

src/pages/index.astro
---
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" />

要使用操作的返回值在成功时向用户显示通知,请将操作传递给 Astro.getActionResult()。使用返回的 data 属性来渲染你想要显示的 UI。

此示例使用 addToCart 操作返回的 productName 属性来显示成功消息。

src/pages/products/[slug].astro
---
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() 返回的值。

src/middleware.ts
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 进行检索。

src/middleware.ts
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

src/actions/index.ts
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”响应。注意:此方法确保操作仅在存在会话时才可访问,但它不是安全授权的替代品。

src/middleware.ts
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.callAction() 包装器(或在使用服务器端点时使用 context.callAction())直接从 Astro 组件脚本中调用操作。这在其他服务器代码中重用你的操作逻辑时很常见。

将操作作为第一个参数传递,任何输入参数作为第二个参数。这将返回与在客户端调用操作时收到的相同的 dataerror 对象。

src/pages/products.astro
---
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
}
---
贡献 社区 赞助