Wasp Actions 实战指南:用声明式代码实现全栈数据变更 📅 发布时间:2026/9/15 21:05:08 👁 浏览次数: Wasp Actions 实战指南用声明式代码实现全栈数据变更【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/waspWasp全栈 Web 开发框架将复杂的全栈能力抽象为声明式配置你只需在 Wasp 文件中声明一个action再在 Node.js 中编写业务逻辑Wasp 就会自动生成服务端路由、客户端调用函数与类型定义。本文以 Wasp 0.15 文档中的 Actions 指南为骨架结合仓库内的真实示例源码完整讲解 Action 的声明、实现、调用、错误处理、实体注入与缓存失效机制帮助你快速掌握这套只写业务逻辑的数据写入工作流。Actions 是什么与 Queries 的分工Wasp 中有两类操作OperationsActions与Queries。二者 API 几乎相同但职责有本质区别Actions 用于修改和新增数据例如给博客文章添加评论、给视频点赞、更新商品价格Queries 只用于读取数据详见 Queries 指南。Actions 与 Queries 协同工作共同维持前端数据缓存的实时性Queries 负责把数据读进来Actions 负责把数据写进去而 Wasp 会在写操作发生后自动让相关读缓存失效并重新拉取。由于 Actions 与 Queries 的 API 高度相似如果你已经熟悉 Queries可以重点阅读下文Queries 与 Actions 的关键区别和API 参考两节。三步走从声明到调用的完整工作流Wasp 中的 Action 遵循一套固定流程在 Wasp 中声明 → 在 Node.js 中实现 → 在任意位置调用。整个过程你不需要自己搭建 HTTP API、处理服务端请求分发也无需操心客户端的响应处理与缓存只需专注于业务逻辑本身。Wasp 在服务端运行 Action但会生成代码让你能在客户端或服务端任何地方通过相同的接口调用它。第一步在 Wasp 中声明 Action在main.wasp文件中使用action声明。以下示例声明了两个 Action一个用于创建任务createTask一个用于将任务标记为完成markTaskAsDone// ... action createTask { fn: import { createTask } from src/actions.js } action markTaskAsDone { fn: import { markTaskAsDone } from src/actions.js }关于action声明支持的所有选项可参考下文API 参考一节。注意Wasp Action 的名称与实现函数的导出名不必一致但为了减少困惑官方建议保持一致。另外此时导入的实现文件还不存在也没关系——先定义高层概念Wasp 声明再处理实现细节Node.js 代码这正是 Wasp 推荐的工作顺序。声明完成后Wasp 会做两件重要的事生成一个与 Action 同名的服务端 Node.js 函数生成一个与 Action 同名的客户端 JavaScript 函数例如markTaskAsDone。该函数接收一个可选参数——一个包含任意可序列化数据的对象Wasp 会通过网络把该对象发送到服务端并作为第一个位置参数传给 Action 实现。这套抽象之所以可行是因为 Wasp 在服务端自动生成了一个 HTTP 路由处理器在底层调用 Action 的 Node.js 实现。这两个生成函数确保了整个应用客户端与服务端拥有一致的调用接口。关于当前仓库的语法说明文档对应的 Wasp 0.15 使用上述.wasp声明语法而本仓库当前版本如 examples/tutorials/TodoAppTs/main.wasp.ts已演进为基于wasp.sh/spec的 TypeScript 声明式语法例如import { action, app, page, query, route } from wasp.sh/spec; import { createTask, updateTask } from ./src/actions with { type: ref }; export default app({ // ... spec: [ // ... action(createTask, { entities: [Task] }), action(updateTask, { entities: [Task] }), ], });两种语法的核心概念一致action声明 entities注入。第二步在 Node.js 中实现 Action既然声明中告诉 Wasp 实现位于src/actions.{js,ts}就从该文件导出实现函数。以下是一个使用内存数组模拟数据库的完整实现// our database let nextId 4 const tasks [ { id: 1, description: Buy some eggs, isDone: true }, { id: 2, description: Make an omelette, isDone: false }, { id: 3, description: Eat breakfast, isDone: false }, ] // 不需要使用参数时可以不声明它 export const createTask (args) { const newTask { id: nextId, isDone: false, description: args.description, } nextId 1 tasks.push(newTask) return newTask } // args 对象由调用方通常来自客户端发送 export const markTaskAsDone (args) { const task tasks.find((task) task.id args.id) if (!task) { // 稍后会介绍如何正确处理这类错误 return } task.isDone true }TypeScript 类型支持在 TypeScript 中Wasp 会根据声明自动生成泛型类型例如CreateTask、MarkTaskAsDone并可从wasp/server/operations导入import { type CreateTask, type MarkTaskAsDone } from wasp/server/operations type Task { id: number description: string isDone: boolean } export const createTask: CreateTaskPickTask, description, Task ( args ) { // ... } export const markTaskAsDone: MarkTaskAsDonePickTask, id, void ( args ) { // ... }生成类型是可选的但强烈推荐使用因为它能正确地推断 Action 的context类型包括context.entities中必须包含的实体、是否含用户信息等它提供两个可选类型参数InputAction 函数接收的参数/载荷类型和OutputAction 函数的返回类型省略类型参数时TypeScript 会推断为最宽泛的类型输入为never输出为unknown若 Action 不需要输入或输出可用void作为类型参数。若不想显式标注返回类型可用satisfies关键字让 TypeScript 自动推断const createFoo (async (_args, context) { const foo await context.entities.Foo.create() return { newFoo: foo, message: Heres your foo!, returnedAt: new Date(), } }) satisfies GetFoo此时 TypeScript 能正确推断context类型以及返回类型{ newFoo: Foo, message: string, returnedAt: Date }。如果连context都不需要可以完全不写类型标注const createFoo () {{ name: Foo, date: new Date() }}说明Wasp 在客户端与服务端之间传输数据时使用 superjson 序列化因此Date等非 JSON 原生类型也能安全地在参数与返回值中传递本文示例中的returnedAt: new Date()即依赖这一能力。第三步在客户端使用 Action在客户端直接从wasp/client/operations导入并调用即可。调用方式与 Action 是否要求认证无关——Wasp 会在后台自动完成已登录用户的认证import { createTask, markTaskAsDone } from wasp/client/operations const newTask await createTask({ description: Learn TypeScript }) await markTaskAsDone({ id: 1 })在 TypeScript 下客户端调用会自动获得完整的全栈类型安全只要在服务端实现中标注了 Action 类型客户端代码就能自动推断返回值类型并校验载荷import { createTask, markTaskAsDone } from wasp/client/operations const newTask await createTask({ description: Keep learning TypeScript }) await markTaskAsDone({ id: 1 })实际开发中Action 通常放在 React 组件里使用。例如在任务页面上通过useQuery读取任务点击按钮时调用markTaskAsDoneimport React from react import { useQuery, getTask, markTaskAsDone } from wasp/client/operations export const TaskPage ({ id }) { const { data: task } useQuery(getTask, { id }) if (!task) { return h1Loading/h1 } const { description, isDone } task return ( div pstrongDescription: /strong{description}/p pstrongIs done: /strong{isDone ? Yes : No}/p {isDone || ( button onClick{() markTaskAsDone({ id })}Mark as done./button )} /div ) }由于 Action 不需要响应式reactive在组件中直接调用是安全的、无需 hook。Wasp 额外提供了useActionhook 来增强 Action如乐观更新详见API 参考。仓库中的真实客户端调用示例可见 examples/tutorials/TodoAppTs/src/MainPage.tsx组件从wasp/client/operations导入createTask、getTasks、updateTask在handleIsDoneChange中调用updateTask({ id, isDone })并捕获错误。第四步在服务端使用 Action在服务端调用 Action 与客户端类似只有两点区别从wasp/server/operations而不是wasp/client/operations导入对需要认证的 Action必须显式传入包含用户信息的context对象。import { createTask, markTaskAsDone } from wasp/server/operations const user // 获取 AuthUser 对象例如从 context.user const newTask await createTask( { description: Learn TypeScript }, { user }, ) await markTaskAsDone({ id: 1 }, { user })错误处理默认 500 与 HttpError出于安全考虑Action 实现中抛出的所有异常默认都会以 HTTP 状态码500返回给客户端且剥离所有错误细节。隐藏错误细节能防止敏感信息通过网络泄露。如果你确实希望把额外错误信息传给客户端可以在实现中构造并抛出HttpErrorimport { HttpError } from wasp/server export const createTask async (args, context) { throw new HttpError( 403, // status code You cant do this!, // message { foo: bar } // data ) }HttpError接收三个参数HTTP 状态码、错误消息、附加数据。仓库中大量真实 Action 都遵循这一模式。例如 examples/tutorials/TodoAppTs/src/actions.ts 中未登录用户调用createTask时抛出HttpError(401)examples/ask-the-documents/src/documents.ts 中也以new HttpError(401, You must be logged in to embed documents)做权限校验。在 Action 中使用 Entities注入 Prisma API大多数 Action 操作的数据都来自 Wasp Entities。要在 Action 中使用实体只需在声明中加入entities字段action createTask { fn: import { createTask } from src/actions.js, entities: [Task] } action markTaskAsDone { fn: import { markTaskAsDone } from src/actions.js, entities: [Task] }Wasp 会把指定的实体注入 Action 的context参数从而让你直接使用该实体的 Prisma API。context.entities.Task暴露的是 Prisma CRUD API即prisma.task。同时Wasp 正是通过每个 Action/Query 所使用的实体来决定失效哪些前端查询缓存见下一节缓存失效。// args 对象是调用方通常来自客户端发送的载荷 export const createTask async (args, context) { const newTask await context.entities.Task.create({ data: { description: args.description, isDone: false, }, }) return newTask } export const markTaskAsDone async (args, context) { await context.entities.Task.update({ where: { id: args.id }, data: { isDone: true }, }) }TypeScript 版本只需加上类型标注即可获得完整的全栈类型安全import { type CreateTask, type MarkTaskAsDone } from wasp/server/operations import { type Task } from wasp/entities export const createTask: CreateTaskPickTask, description, Task async ( args, context ) { const newTask await context.entities.Task.create({ data: { description: args.description, isDone: false, }, }) return newTask } export const markTaskAsDone: MarkTaskAsDonePickTask, id, void async ( args, context ) { await context.entities.Task.update({ where: { id: args.id }, data: { isDone: true }, }) }仓库中更复杂的实体操作示例可见 examples/kitchen-sink/src/features/operations/actions.tscreateTask使用Task.create并通过user: { connect: { id: context.user.id } }关联当前用户updateTaskIsDone使用Task.updateMany按用户维度更新deleteCompletedTasks与toggleAllTasks则分别演示了deleteMany与count/updateMany的组合使用。缓存失效基于实体的自动失效机制管理 Web 应用状态最棘手的问题之一就是保证 Query 返回的数据始终是最新的。Wasp 使用 react-query 管理查询因此必须确保查询在数据变旧后及时失效。虽然 react-query 本身提供了手动失效手段如 refetch、直接 invalidation但手动管理缓存很快会变得复杂且易错。Wasp 提供了更高效的方案基于实体的自动 Query 缓存失效。由于 Action 通常且应该修改状态、而 Query 读取状态Wasp 会在某个使用了某实体的 Action 被执行时自动失效所有使用了同一实体的 Query 缓存。例如createTask与getTasks都使用实体Task执行createTask后getTasks的缓存结果可能已过期Wasp 会自动使其失效并触发重新拉取。实践中这意味着你无需思考缓存失效Wasp 会让查询保持新鲜。当然这种自动失效也有代价它可能产生一些不必要的更新且只对实体有效。如果遇到这类问题可以使用 react-query 提供的机制手动处理期待 Wasp 后续版本提供更优雅的原生支持。若希望在执行 Action 后乐观地设置缓存值可参考下文useActionhook 中的乐观更新配置。Queries 与 Actions 的关键区别Actions 和 Queries 是 Wasp 中紧密相关的两个概念表面上功能相似但 Wasp 对它们的处理方式不同。关键区别有三点Actions 可以且通常应该修改服务端状态而 Queries 只允许读取。Wasp 执行缓存失效时依赖这一约定因此务必遵守——不要把写操作放进 Query 里Actions 不需要响应式可以直接调用。不过 Wasp 提供了useActionReact hook用于为 Action 附加额外行为如乐观更新action声明与query声明几乎完全一致唯一的区别是声明关键字的名字。API 参考在 Wasp 中声明 Actionaction声明支持以下字段字段必填说明fn: ExtImport是Action 的 Node.js 实现的导入语句entities: [Entity]否希望在 Action 内部使用的实体列表用法见上文在 Action 中使用 Entities声明示例action createFoo { fn: import { createFoo } from src/actions.js entities: [Foo] }声明后即可在代码任意位置服务端或客户端导入使用// 在客户端使用 import { createFoo } from wasp/client/operations // 在服务端使用 import { createFoo } from wasp/server/operationsTypeScript 下还有对应的类型导入import { type CreateFoo } from wasp/server/operations实现 Action函数签名Action 的实现是一个 Node.js 函数接收两个位置参数需要await时可以是async函数。参数名可以任意命名文档统一使用args和contextargs类型取决于 Action包含调用 Action 时传入的数据例如过滤条件context类型取决于 Action由 Wasp 注入的上下文对象包含用户会话信息与实体信息。context.entities的用法见上文context.user的用法可参考 Auth 文档。TypeScript 下声明为createSomething的 Action 会生成泛型类型CreateSomething包含两个可选类型参数Inputargs的类型默认never与Output返回值类型默认unknown。默认值刻意选择了最宽松的类型如果 Action 不需要输入/输出使用void作为类型参数即可。import { type CreateFoo } from wasp/server/operations type Foo // ... export const createFoo: CreateFoo{ bar: string }, Foo (args, context) { // implementation };上例中Action 期望接收一个含bar: string字段的对象args的类型并返回Foo类型的值。useActionHook 与乐观更新阅读本节前请先理解 Queries 与上文缓存失效的机制。在组件中使用 Action 时可以用useActionhook 增强它。该 hook 随 Wasp 内置用于装饰 Action——它返回一个与原 Action 接口一致、但底层附加了额外行为的函数。useAction接收两个参数actionFn必填希望增强的 Wasp Action即 Wasp 根据声明生成的客户端 Action 函数actionOptions配置附加功能的选项对象。虽然技术上可选但不提供它就没有使用 hook 的意义与直接调用 Action 无异。该对象支持一个字段optimisticUpdates一个对象数组每个对象定义一个针对 Query 缓存的乐观更新包含两个必填属性getQuerySpecifier返回 Query 标识符用于定位要更新的 Query 的值的函数。Query 标识符是一个数组由查询函数与其参数组成。例如要乐观更新useQuery(fetchFilteredTasks, { isDone: true })对应的缓存getQuerySpecifier应返回[fetchFilteredTasks, { isDone: true }]。Wasp 会把传给被装饰 Action 的参数转发给该函数因此可以利用新增/变更条目的属性来定位 QueryupdateQuery执行乐观更新的函数应返回缓存的目标状态。Wasp 会以两个参数调用它item传给被装饰 Action 的参数与oldData标识符所定位 Query 的当前缓存值。注意updateQuery必须是纯函数——只返回getQuerySpecifier定位的缓存目标值不得有任何副作用。同时请只更新确实被该 Action 影响的 Query 缓存Wasp 目前无法校验这一点。最后updateQuery的实现应能正确处理任意oldData状态例如不要依赖数组位置。如果乐观更新期间还需要做其他事情可以直接使用 react-query 的底层 API见下文高级用法。以下示例为markTaskAsDone切换任务isDone状态配置乐观更新import React from react import { useQuery, useAction, getTask, markTaskAsDone, } from wasp/client/operations const TaskPage ({ id }) { const { data: task } useQuery(getTask, { id }) const markTaskAsDoneOptimistically useAction(markTaskAsDone, { optimisticUpdates: [ { getQuerySpecifier: ({ id }) [getTask, { id }], updateQuery: (_payload, oldData) ({ ...oldData, isDone: true }), }, ], }) // ...渲染部分点击按钮时调用 markTaskAsDoneOptimistically({ id }) }TypeScript 版本可通过OptimisticUpdateDefinition泛型获得类型检查import { useQuery, useAction, type OptimisticUpdateDefinition, getTask, markTaskAsDone, } from wasp/client/operations import type { Task } from wasp/entities type TaskPayload PickTask, id; const markTaskAsDoneOptimistically useAction(markTaskAsDone, { optimisticUpdates: [ { getQuerySpecifier: ({ id }) [getTask, { id }], updateQuery: (_payload, oldData) ({ ...oldData, isDone: true }), } as OptimisticUpdateDefinitionTaskPayload, Task, ], });高级用法useActionhook 目前仅支持乐观更新。Wasp 的乐观更新 API 刻意保持精简专注于 Query 缓存更新这一最常见场景。如果你需要更灵活或更高控制力的 API可以放弃useAction改用 react-query 的useMutationhook 并直接使用其底层 API。此时你需要拿到 Query 缓存的 key。Wasp 内部使用该 key 但对开发者做了抽象不过你可以通过任意 Query 的queryCacheKey属性轻松获取import { getTasks } from wasp/client/operations const queryKey getTasks.queryCacheKey仓库提示在 examples/kitchen-sink/src/features/operations/actions.ts 的updateTaskIsDone中有一段被注释的sleep(2000)延迟代码注释写着 Uncomment to test optimistic updates——这是观察乐观更新生效过程的实用调试技巧恢复延迟后界面上会先看到乐观更新立即生效随后服务端响应返回后缓存被真实数据覆盖。总结Wasp 的 Action 机制把数据写入这条完整链路——声明、服务端实现、HTTP 路由、客户端调用函数、类型生成、缓存失效——全部封装进声明式配置中。你只需要记住四个关键动作声明actionfnentities、实现Node.js 函数善用HttpError与生成的类型、调用客户端从wasp/client/operations、服务端从wasp/server/operations导入、增强用useAction做乐观更新。配合基于实体的自动缓存失效Actions 与 Queries 协同即可让前端数据始终新鲜把工程复杂度留给框架把开发精力留给业务。想深入实践可以在本仓库中对照阅读TodoApp 教程示例最小完整的 Action 声明 实现 客户端调用、kitchen-sink 综合示例覆盖create/updateMany/deleteMany/count等 Prisma 操作与权限校验以及 ask-the-documents 示例HttpError在实际业务中的典型用法。【免费下载链接】waspThe batteries-included full-stack framework for the AI era. Develop JS/TS web apps (React, Node.js, and Prisma) using declarative code that abstracts away complex full-stack features like auth, background jobs, RPC, email sending, end-to-end type safety, single-command deployment, and more.项目地址: https://gitcode.com/GitHub_Trending/wa/wasp创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考