资讯动态

Wasp 框架 Actions 完全指南:声明式后端操作、实体注入与 Query 缓存自动失效

发布时间:2026/9/16 21:35:25 来源:尧图企业网站定制
Wasp 框架 Actions 完全指南声明式后端操作、实体注入与 Query 缓存自动失效【免费下载链接】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导读本文是 Wasp 数据操作Operations体系的核心技术指南围绕 version-0.19 版本文档 系统讲解Actions如何通过action声明在main.wasp中定义写操作、如何在 Node.js 中实现其业务逻辑、如何在客户端与服务端统一调用以及 Wasp 如何基于实体自动完成前端 Query 缓存失效、并通过useActionHook 实现乐观更新。读完本文你将掌握在 Wasp 中编写增改数据类后端逻辑的完整实战方案并理解其底层代码生成机制与全栈类型安全原理。什么是 Actions在 Wasp 中数据操作Operations分为两类Queries与Actions二者共同构成了 Operations 概述 中提到的围绕数据模型工作的能力层Queries只读数据如获取任务列表、查询用户信息。Actions修改与新增数据如给博客文章添加评论、点赞视频、更新商品价格。Actions 与 Queries 的 API 几乎完全一致官方文档明确提示熟悉 Queries 的开发者可以跳过大部分内容只读Queries 与 Actions 的区别一节。二者协同工作共同保证前端数据缓存始终新鲜Actions 负责改变服务端状态Queries 负责读取而 Wasp 会在 Action 执行后自动使相关 Query 缓存失效。在 Wasp 编译器内部Queries 与 Actions 被统一建模为Operation类型见 waspc/src/Wasp/AppSpec/Operation.hsdata Operation QueryOp String Query | ActionOp String Action这正是两者在 API 层面几乎相同、仅在声明名称上不同这一设计的底层来源。使用 Actions 的两步工作流Actions 在 Wasp 中声明、在 Node.js 中实现。Wasp 会在服务端上下文server context中运行 Actions同时生成代码让你在任意位置客户端或服务端以相同接口调用它们在 Wasp 文件中使用action声明 Action。在 Node.js 中实现 Action 的业务逻辑。完成后即可在代码的任何位置使用该 Action。你完全不需要关心构建 HTTP API、管理服务端请求处理、处理客户端响应与缓存——只需专注业务逻辑其余交给 Wasp。从代码生成的角度看服务端生成器会为每个 Action 产出一个包装文件导入用户实现、注入上下文后重新导出见 waspc/src/Wasp/Generator/ServerGenerator/OperationsG.hs。这也是其余交给 Wasp的实现基础。声明 Actions在main.wasp中以action声明开始。例如声明两个 Action——一个用于创建任务一个用于将任务标记为完成// ... action createTask { fn: import { createTask } from src/actions } action markTaskAsDone { fn: import { markTaskAsDone } from src/actions }注意Wasp 中的action声明与其 Node.js 实现的名称不必相同fn字段指向具体实现但为免混淆官方示例统一保持同名。Wasp 声明的名称与实现无关这一点在仓库中有直接佐证examples/kitchen-sink中声明的 Action 与实现保持同名如 operations.wasp.ts 中的createTask、updateTaskIsDone、deleteCompletedTasks、toggleAllTasksexamples/waspello中的 cards.wasp.ts 则展示了可同时声明entities的写法。小提示你可能会发现上例中导入的 Action 实现尚不存在。无需担心下一步就在src/actions.{js,ts}中编写这些实现。官方建议遵循先高层概念Wasp 声明、后实现细节JS 实现的开发顺序。声明完成后Wasp 会自动做两件重要的事生成一个与服务端同名的 Node.js 函数用于在服务端逻辑中调用生成一个与 Action 同名的客户端 JavaScript 函数例如markTaskAsDone。该函数接收一个可选参数——包含任意可序列化数据的对象。Wasp 会将该对象通过网络发送并作为第一个位置参数传入 Action 实现。这套抽象依赖 Wasp 在服务端生成的 HTTP API 路由处理器它会在底层调用 Action 的 Node.js 实现。生成这两个函数保证了整个应用客户端与服务端拥有一致的调用接口。在 Node 中实现 Actions我们已经指示 Wasp 从src/actions.{js,ts}中寻找实现因此需要在该文件中导出对应函数。以下是如何实现之前声明的createTask与markTaskAsDone// 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 版本利用 Wasp 自动生成的泛型类型import { type CreateTask, type MarkTaskAsDone } from wasp/server/operations type Task { id: number description: string isDone: boolean } // 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: CreateTaskPickTask, description, Task ( args ) { const newTask { id: nextId, isDone: false, description: args.description, } nextId 1 tasks.push(newTask) return newTask } // args 对象由调用方通常是客户端发送 export const markTaskAsDone: MarkTaskAsDonePickTask, id, void ( args ) { const task tasks.find((task) task.id args.id) if (!task) { // 稍后会展示如何正确处理此类错误 return } task.isDone true }载荷约束superjsonWasp 底层使用superjson进行序列化。这意味着你不局限于只能收发 JSON 载荷Wasp 会自动处理 superjson 支持的所有数据类型 的序列化与反序列化如bigint、Date、Map、Set等以及Prisma.Decimal在 TypeScript 中只要你使用正确的自动生成类型标注 Operations编译器会确保载荷合法即 Wasp 知道如何序列化/反序列化它们。Actions 的类型支持TypeScriptWasp 会根据main.wasp中的声明自动生成类型CreateTask与MarkTaskAsDoneCreateTask是基于createTask的 Action 声明自动生成的泛型类型MarkTaskAsDone是基于markTaskAsDone的 Action 声明自动生成的泛型类型。使用这些类型标注实现是可选的但非常有用——它能让 Action 的context获得正确类型TypeScript 会知道context.entities必须包含Task实体也会根据 Action 是否使用 auth 判断context是否包含用户信息。生成的类型是泛型接受两个可选类型参数Input—— Action 函数接收的参数载荷。Output—— Action 函数的返回类型。以上例说明createTask期望接收包含新任务描述的对象输入类型为PickTask, description并返回新任务输出类型为TaskmarkTaskAsDone期望接收类型为PickTask, id的对象派生自Task实体类型。如果不在乎输入输出类型可以省略两个类型参数TypeScript 会推断最宽泛的类型输入为never输出为unknown。虽然完全可选但官方强烈建议显式指定因为可以带来实现内部对参数和返回值的类型支持全栈类型安全在客户端调用时体现。提示推断返回类型。如果不希望显式写出 Action 的返回类型可以用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 CreateFoo从上述代码中TypeScript 能知道context的正确类型以及 Action 返回类型为{ newFoo: Foo, message: string, returnedAt: Date }。如果不需要context可以连 Action 的类型和参数一起省略const createFoo () ({ name: Foo, date: new Date() })使用 Actions在客户端使用 Actions在客户端调用 Action只需从wasp/client/operations导入并直接调用import { createTask, markTaskAsDone } from wasp/client/operations // ... const newTask await createTask({ description: Learn TypeScript }) await markTaskAsDone({ id: 1 })TypeScript 版本会自动推断返回值并类型校验载荷import { createTask, markTaskAsDone } from wasp/client/operations const newTask await createTask({ description: Keep learning TypeScript }) await markTaskAsDone({ id: 1 })Wasp 支持自动的全栈类型安全只需在服务端定义中指定 Action 的类型客户端代码便会自动获得其 API 载荷类型。使用方式不依赖 Action 是否经过认证——Wasp 会在后台自动认证当前登录用户。在客户端使用 Actions 时最典型的场景是组件内部。以下是一个标记任务完成的组件import 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 p strongDescription: /strong {description} /p p strongIs done: /strong {isDone ? Yes : No} /p {isDone || ( button onClick{() markTaskAsDone({ id })}Mark as done./button )} /div ) }TypeScript 版本与之几乎相同只是为组件props标注类型({ id }: { id: number })。由于 Actions 不需要响应式reactive在组件内无需 Hook 即可直接使用。当然Wasp 也提供了useActionHook 来增强 Action详见后文 API Reference。在服务端使用 Actions在服务端调用 Action 与客户端类似只需两处不同从wasp/server/operations而不是wasp/client/operations导入对于需要认证的 Action必须传入包含 user 的 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 })TypeScript 版本同样会自动推断返回值并校验载荷。关于context.user对象的使用含hashedPassword字段会被剥离等安全细节可参阅 auth 文档的 Using the context.user object 小节。错误处理出于安全考虑Action 的 Node.js 实现中抛出的所有异常都会以 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 ) }TypeScript 版本import { type CreateTask } from wasp/server/operations import { HttpError } from wasp/server export const createTask: CreateTask async (args, context) { throw new HttpError( 403, // status code You cant do this!, // message { foo: bar } // data ) }仓库中的真实示例展示了HttpError在认证场景的典型用法examples/kitchen-sink的 actions.ts 在context.user不存在时抛出HttpError(401)并将新任务与当前用户通过connect: { id: context.user.id }关联——这正是每个操作必须检查context.user并决定如何处理这一访问控制约定的落地实现。在 Actions 中使用实体Entities大多数情况下Action 中操作的数据资源是 Entities。要在 Action 中使用实体把它添加到 Wasp 的action声明中action createTask { fn: import { createTask } from src/actions, entities: [Task] } action markTaskAsDone { fn: import { markTaskAsDone } from src/actions, entities: [Task] }Wasp 会将指定实体注入 Action 的context参数使你可以访问该实体的 Prisma API。同时Wasp 通过检查每个 Action/Query 使用的实体来失效前端 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 版本标注 Action 类型仍可选但能显著提升全栈类型安全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 }, }) }context.entities.Task对象暴露的是 Prisma 的 CRUD API对应prisma.task。从 Action 声明的数据结构看Action类型由fn :: ExtImport与entities :: Maybe [Ref Entity]两个字段构成见 waspc/src/Wasp/AppSpec/Action.hs——这就是action声明支持的全部配置项。缓存失效Cache InvalidationWeb 应用状态管理中最棘手的问题之一是保证 Queries 返回的数据始终最新。由于 Wasp 使用react-query管理 Query必须在数据过期时使 Query更准确地说是 react-query 管理的缓存结果失效。你可以通过 react-query 提供的多种机制手动失效缓存如 refetch、直接 invalidation。但手动缓存失效很快会变得复杂且容易出错因此 Wasp 提供了一种更快捷高效的方案基于实体的自动 Query 缓存失效。由于 Actions 可以且大多数时候确实会修改状态而 Queries 读取状态因此Wasp 会在某个使用相同实体的 Action 执行后使该 Query 的缓存失效。例如若 ActioncreateTask与 QuerygetTasks都使用实体Task则执行createTask后getTasks的缓存结果可能过期——Wasp 会立即使其失效触发getTasks从服务端重新拉取并更新数据。在实践中这意味着无需考虑缓存失效Wasp 就能让 Queries 保持新鲜。当然这种自动失效有时会显得浪费某些更新可能并无必要且只对实体生效。如果遇到这类问题可以暂时使用 react-query 提供的机制Wasp 未来版本会以更优雅的方式支持这些场景。仓库佐证失效的时序保证。examples/kitchen-sink中的 cacheInvalidation.test.ts 专门针对 GitHub issue #3009 编写了回归测试当 Action 声明entities: [X]且某个 Query 也依赖X时Action 的 Promise 完成时 Query 缓存必须已经反映更新即失效触发的 refetch 必须在await someAction()返回前完成。测试在await createTask({ description: after })之后同步断言getTasks缓存已包含新任务验证了Action 执行后相关 Query 立即刷新这一契约。如果你希望在执行 Action 后乐观地optimistically设置缓存值可以使用乐观更新机制通过 Wasp 的 useAction Hook 配置。这是目前 Wasp 原生支持的唯一手动缓存失效机制其他场景可以始终依赖 react-query。Queries 与 Actions 的区别Actions 与 Queries 是 Wasp 中两个紧密相关的概念。它们看似执行类似任务但 Wasp 对二者的处理方式不同各自代表不同的语义。核心区别如下Actions 可以且通常应该修改服务端状态而 Queries 只允许读取。Wasp 在执行缓存失效时依赖你遵守这一约定因此务必遵循。Actions 不需要响应式可以直接调用。不过 Wasp 提供了useActionReact Hook用于为 Action 附加额外行为如乐观更新。action声明与query声明几乎完全一致唯一区别在于声明的名称。从编译器源码看二者都走 OperationsG.hs 的同一套genOperations管线genQueries genActions只是模板文件不同_query.ts与_action.ts。API Reference在 Wasp 中声明 Actionsaction声明支持以下字段fn: ExtImport必填Action 的 Node.js 实现的导入语句。entities: [Entity]希望在 Action 中使用的实体列表。使用方式见在 Actions 中使用实体一节。示例声明 Actionaction createFoo { fn: import { createFoo } from src/actions entities: [Foo] }之后便可在代码的任何位置服务端或客户端导入并使用它// 在客户端使用 import { createFoo } from wasp/client/operations // 在服务端使用 import { createFoo } from wasp/server/operationsTypeScript 还可以在服务端导入对应的类型import { type CreateFoo } from wasp/server/operations实现 ActionsAction 的实现是一个接收两个参数的 Node.js 函数如需使用await关键字可以写成async函数。由于两个参数都是位置参数你可以随意命名但官方约定为args与contextargs类型取决于 Action包含调用 Action 时传入的数据的对象例如过滤条件。参见使用 Actions中的示例了解如何传递该对象。context类型取决于 Action由Wasp 注入 Action 的附加上下文对象包含用户会话信息以及实体信息。参见在 Actions 中使用实体了解context对象的entities字段用法或 auth 文档 了解user对象用法。TypeScript 类型支持声明 Action 后Wasp 会生成一个可用于定义实现的泛型类型。对于声明为createSomething的 Action生成的类型名为CreateSomethingimport { type CreateSomething } from wasp/server/operations它接受两个可选类型参数Input——args对象的类型Action 的输入载荷默认值为never。Output—— Action 返回值的类型Action 的输出载荷默认值为unknown。默认值的设计初衷是让类型签名尽可能宽松。如果不想让 Action 接收/返回任何内容请使用void作为类型参数。示例以下声明action createFoo { fn: import { createFoo } from src/actions entities: [Foo] }期望从src/actions.js文件中找到命名导出createFooexport const createFoo (args, context) { // implementation }TypeScript 版本使用生成类型CreateFoo并通过类型参数指定输入输出import { type CreateFoo } from wasp/server/operations type Foo // ... export const createFoo: CreateFoo{ bar: string }, Foo (args, context) { // implementation }此例中Action 期望接收一个含bar: string字段的对象即args的类型并返回类型为Foo的值必须与 Action 实际返回值匹配。useActionHook 与乐观更新阅读本章前请先理解 Queries 与缓存失效的工作原理。在组件中使用 Actions 时可以借助 Wasp 内置的useActionHook 增强它们。该 Hook 用于装饰 Wasp Actions——返回一个 API 与原始 Action 一致的函数同时在底层做额外的事情取决于你的配置。useAction接收两个参数actionFn必填要增强的 Wasp Action即 Wasp 根据 Action 声明生成的客户端 Action 函数。actionOptions一个配置对象用于指定要附加到 Action 的额外功能。虽然技术上可选但不传它就没有使用useAction的意义与直接调用 Action 无异。支持以下字段optimisticUpdates一个对象数组每个对象定义对 Query 缓存执行的乐观更新。定义乐观更新必须指定以下属性getQuerySpecifier必填返回 Query specifier 的函数specifier 是用于定位要更新的 Query 的值。Query specifier 是一个指定 query 函数及其参数的数组。例如要为useQuery(fetchFilteredTasks, { isDone: true })使用的 Query 做乐观更新getQuerySpecifier需要返回数组[fetchFilteredTasks, { isDone: true }]。Wasp 会将传入被装饰 Action 的参数转发给该函数你可以利用新增/变更项的属性来定位 Query。updateQuery必填执行乐观更新的函数应返回缓存的期望状态。Wasp 会用以下参数调用它item—— 传入被装饰 Action 的参数oldData—— specifier 标识的 Query 当前缓存值。注意updateQuery函数必须是纯函数——返回getQuerySpecifier定位的期望缓存值且不得产生任何副作用。同时请确保只更新受当前 Action 影响的 Query 缓存Wasp 目前无法校验这一点。最后updateQuery的实现应能正确处理任何oldData状态例如不要依赖数组位置。如果在乐观更新期间需要做其他事情可以直接使用 react-query 的底层 API见高级用法。以下是配置markTaskAsDoneAction将任务的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 }), }, ], }) if (!task) { return h1Loading/h1 } const { description, isDone } task return ( div p strongDescription: /strong {description} /p p strongIs done: /strong {isDone ? Yes : No} /p {isDone || ( button onClick{() markTaskAsDoneOptimistically({ id })} Mark as done. /button )} /div ) } export default TaskPageTypeScript 版本使用OptimisticUpdateDefinition类型提供类型检查import React from react import { useQuery, useAction, type OptimisticUpdateDefinition, getTask, markTaskAsDone, } from wasp/client/operations type TaskPayload PickTask, id const TaskPage ({ id }: { id: number }) { const { data: task } useQuery(getTask, { id }) // TypeScript 会自动类型校验载荷类型 const markTaskAsDoneOptimistically useAction(markTaskAsDone, { optimisticUpdates: [ { getQuerySpecifier: ({ id }) [getTask, { id }], updateQuery: (_payload, oldData) ({ ...oldData, isDone: true }), } as OptimisticUpdateDefinitionTaskPayload, Task, ], }) if (!task) { return h1Loading/h1 } const { description, isDone } task return ( div p strongDescription: /strong {description} /p p strongIs done: /strong {isDone ? Yes : No} /p {isDone || ( button onClick{() markTaskAsDoneOptimistically({ id })} Mark as done. /button )} /div ) } export default TaskPage仓库实例examples/kitchen-sink的 Todo.tsx 中useAction(updateTaskIsDone, ...)的updateQuery处理了oldData undefined缓存为空的分支——这正是官方文档强调应正确处理任何oldData状态的实际写法。高级用法useActionHook 目前仅支持乐观更新Wasp 未来版本会带来更多特性。Wasp 的乐观更新 API 刻意保持小巧专注于更新 Query 缓存这是最常见的用例。如果你需要更灵活或更高控制级别的 API可以放弃 Wasp 的useActionHook改用 react-query 的useMutationHook 直接操作其底层 API。如果决定直接使用 react-query 的 API你需要访问 Query 缓存键。Wasp 内部使用该键但对开发者做了抽象你可以通过访问任意 Query 上的queryCacheKey属性轻松获得它import { getTasks } from wasp/client/operations const queryKey getTasks.queryCacheKey小结Actions 是 Wasp 数据操作体系中负责写的半壁江山声明一个action、实现一个 Node.js 函数即可同时获得服务端 HTTP 路由、客户端调用函数、类型安全与自动缓存失效。理解其与 Queries 的分工、实体注入机制以及useAction的乐观更新能力是构建数据驱动型 Wasp 应用的关键。想继续深入可阅读 Queries 文档、Entities 文档 与 Operations 概述并在 examples/kitchen-sink 与 examples/waspello 中查看完整的可运行示例。【免费下载链接】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),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价