
Wasp 教程用 Action 修改数据——从声明到全栈类型安全的完整实战【免费下载链接】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 官方教程 0.12 版《6. Modifying Data》06-actions.md为主体完整讲解如何声明、实现并调用 Wasp Action 来创建和更新数据库记录包括在 Wasp 入口文件中声明 Action、在src/actions中实现服务端逻辑、在 React 组件中直接调用 Action以及 Wasp 内置的“Query 自动失效”机制。读完本篇你可以独立为 Wasp 应用编写任意写操作创建/更新任务理解 Action 与 Query 在调用方式上的差异并能在源码层面验证“Action 成功后 Query 自动刷新”这一机制的底层实现。为什么需要 Action在教程的上一节中我们学习了用 Query 从数据库读取数据只是简单提过 Action 可以更新数据库。Action 是 Wasp 的“写操作”抽象它运行在服务端通过 Prisma 修改数据库并且与 Query 一样自动获得前后端类型推导。本节的目标是完成一个 Todo 应用的两个写操作一个创建新任务的 Wasp ActioncreateTask一个在用户提交表单时调用该 Action 的 React 表单一个更新任务状态的 ActionupdateTask配合复选框完成“标记为已完成”。声明一个 Action创建 Action 与创建 Query 非常相似。首先必须在 Wasp 入口文件main.wasp0.12 版教程使用此声明式语法中声明// ... action createTask { fn: import { createTask } from src/actions, entities: [Task] }声明包含两个关键部分fn:指定服务端实现函数这里从src/actions模块导入createTask。注意导入路径必须能被 Wasp 解析到src目录内的真实文件entities: [Task]声明该 Action 会读写Task实体。这个字段不只是元数据——它是 Wasp 实现“自动 Query 失效”的依据后文详述。版本说明仓库中当前的示例项目已迁移到 TypeScript specmain.wasp.ts等价的声明写法是action(createTask, { entities: [Task] })与action(updateTask, { entities: [Task] })并通过import { createTask, updateTask } from ./src/actions with { type: ref }引入实现。两种写法表达的是同一个概念。实现 Action声明之后在服务端定义对应的 JS/TS 函数。JavaScript 版本src/actions.jsexport const createTask async (args, context) { return context.entities.Task.create({ data: { description: args.description }, }) }TypeScript 版本src/actions.tsimport { Task } from wasp/entities import { CreateTask } from wasp/server/operations type CreateTaskPayload PickTask, description export const createTask: CreateTaskCreateTaskPayload, Task async ( args, context ) { return context.entities.Task.create({ data: { description: args.description }, }) }实现函数接收两个参数args客户端调用createTask({ description })时传入的参数对象contextWasp 注入的服务端上下文其中context.entities暴露了 Prisma 客户端的实体操作create/update/findMany等。TS 版本中我们用 Wasp 生成的CreateTask类型标注了实现。正如 Query 一节所述在服务端实现上标注类型后这些类型会传递到前端从而获得全栈类型安全。这些类型并非手写而是由 Wasp 编译器根据main.wasp中的声明自动生成——对应的模板源码见 wrappers.ts它把你在 spec 中声明的 operation definition 转换为CreateTaskPayload, Return这样的服务端 API 类型再推导为客户端可感知的调用签名。:::tip 教程把函数放在新建的src/actions.{js,ts}文件中但其实可以放在任何位置这里没有限制只要 Wasp 文件中的声明正确导入了该函数且文件位于src目录内即可。仓库示例 actions.ts 中createTask和updateTask就都集中定义在这个文件里。 :::在客户端调用 Action与 Query 需要用useQuery等 Hook 包裹不同Action 可以直接调用不需要 Hook 包裹因为我们不需要响应式的数据获取只需要发起一次写请求。其余部分就是普通的 React 代码。先定义创建任务的表单import { createTask, getTasks, useQuery } from wasp/client/operations // ... MainPage, TaskView, TaskList ... const NewTaskForm () { const handleSubmit async (event) { event.preventDefault() try { const target event.target const description target.description.value target.reset() await createTask({ description }) } catch (err) { window.alert(Error: err.message) } } return ( form onSubmit{handleSubmit} input namedescription typetext defaultValue / input typesubmit valueCreate task / /form ) }TypeScript 版本src/MainPage.tsximport { FormEvent } from react import { Task } from wasp/entities import { createTask, getTasks, useQuery } from wasp/client/operations // ... MainPage, TaskView, TaskList ... const NewTaskForm () { const handleSubmit async (event: FormEventHTMLFormElement) { event.preventDefault() try { const target event.target as HTMLFormElement const description target.description.value target.reset() await createTask({ description }) } catch (err: any) { window.alert(Error: err.message) } } return ( form onSubmit{handleSubmit} input namedescription typetext defaultValue / input typesubmit valueCreate task / /form ) }在 TS 版本下由于此前已用正确类型标注了 Action 的服务端实现Wasp 知道createTask期望的参数类型是{ description: string }试着改一下参数编辑器会直接报错并且知道调用createTask会返回一个Task本例未使用返回值。仓库中的真实实现 MainPage.tsx 与此结构一致——NewTaskForm组件在提交后await createTask({ description })出错时通过window.alert提示。最后把表单加入页面组件import { FormEvent } from react import { Task } from wasp/entities import { createTask, getTasks, useQuery } from wasp/client/operations const MainPage () { const { data: tasks, isLoading, error } useQuery(getTasks) return ( div NewTaskForm / {tasks TasksList tasks{tasks} /} {isLoading Loading...} {error Error: error} /div ) } // ... TaskList, TaskView, NewTaskForm ...现在创建一个“Build a Todo App in Wasp”任务它会出现在下方列表中——任务在服务端创建并保存到数据库。刷新页面或换一个浏览器打开任务依然还在自动 Query 失效机制创建新任务后任务列表自动刷新以显示新任务——尽管我们没有写任何一行刷新代码。Wasp 在底层处理了这些自动更新声明getTasks和createTask时我们都指定了它们使用Task实体因此当createTask成功完成后Wasp 知道getTasks所读取的数据可能已变化会在后台自动重新执行该 Query也就是说开箱即用Wasp 会让所有 Query 与任何通过 Action 做出的修改保持同步。这一行为的源码印证可以在 Wasp 生成代码的客户端模板中找到。核心逻辑在 resources.js// 每个 Query 首次执行时会把它用了哪些资源(实体)登记进这张映射表 // key 为资源名(如 Task)value 为使用该资源的 query cache key 集合 const resourceToQueryCacheKeys new Map() export function addResourcesUsedByQuery(queryCacheKey, resources) { ... } // Action 完成后的收尾处理 export async function registerActionDone(resources, optimisticUpdateTuples) { optimisticUpdateTuples.forEach(({ queryKey }) updateHandlers.remove(queryKey)) await invalidateQueriesUsing(resources) } // 精确失效找出所有使用了指定实体的 query逐个 invalidate async function invalidateQueriesUsing(resources) { const queryClient await queryClientInitialized const queryCacheKeysToInvalidate getQueriesUsingResources(resources) await Promise.all( queryCacheKeysToInvalidate.map( queryCacheKey queryClient.invalidateQueries(queryCacheKey) ) ) }从源码结构看整套机制是Query 执行时向resourceToQueryCacheKeys注册自己依赖的实体名Action 成功返回时调用registerActionDone按entities: [Task]声明反查出所有依赖Task的 Query cache key再通过 TanStack Query 的invalidateQueries触发后台重取。这正是entities字段在客户端侧的实际用途。需要说明的限制教程原文指出这种“全量失效”作为默认行为很方便但在大型应用中可能带来性能开销原文当时表示尚无覆盖该行为的机制属于 Wasp 计划在未来引入的功能。第二个 ActionupdateTask一个不能标记任务为已完成的 Todo 应用是不完整的。这里不再逐步演示——既然createTask已经完成建议你亲手实现一个名为updateTask的 Action它接收任务的id和isDone状态并在 React 侧复选框被切换时调用它。参考答案如下。声明main.wasp// ... action updateTask { fn: import { updateTask } from src/actions, entities: [Task] }服务端实现JavaScript 版本src/actions.js// ... export const updateTask async ({ id, isDone }, context) { return context.entities.Task.update({ where: { id }, data: { isDone: isDone, }, }) }TypeScript 版本src/actions.tsimport { CreateTask, UpdateTask } from wasp/server/operations // ... type UpdateTaskPayload PickTask, id | isDone export const updateTask: UpdateTaskUpdateTaskPayload, Task async ( { id, isDone }, context ) { return context.entities.Task.update({ where: { id }, data: { isDone: isDone, }, }) }然后就可以在 React 组件中调用updateTask了。在TaskView中给复选框绑定onChange处理器// ... import { updateTask, createTask, getTasks, useQuery, } from wasp/client/operations // ... MainPage ... const TaskView ({ task }) { const handleIsDoneChange async (event) { try { await updateTask({ id: task.id, isDone: event.target.checked, }) } catch (error) { window.alert(Error while updating task: error.message) } } return ( div input typecheckbox id{String(task.id)} checked{task.isDone} onChange{handleIsDoneChange} / {task.description} /div ) } // ... TaskList, NewTaskForm ...TypeScript 版本src/MainPage.tsximport { FormEvent, ChangeEvent } from react import { Task } from wasp/entities import { updateTask, createTask, getTasks, useQuery, } from wasp/client/operations // ... MainPage ... const TaskView ({ task }: { task: Task }) { const handleIsDoneChange async (event: ChangeEventHTMLInputElement) { try { await updateTask({ id: task.id, isDone: event.target.checked, }) } catch (error: any) { window.alert(Error while updating task: error.message) } } return ( div input typecheckbox id{String(task.id)} checked{task.isDone} onChange{handleIsDoneChange} / {task.description} /div ) } // ... TaskList, NewTaskForm ...进阶视角从单用户到多用户的实现演进教程原文以“是时候做最后一个补充了支持多用户”收尾。作为对照仓库中当前版本的 actions.ts 展示了加上认证后这两个 Action 的形态——写操作依然通过context.entities完成只是多了权限边界export const createTask: CreateTaskCreateTaskPayload, Task async ( args, context, ) { if (!context.user) { throw new HttpError(401); } return context.entities.Task.create({ data: { description: args.description, user: { connect: { id: context.user.id } }, }, }); }; export const updateTask: UpdateTaskUpdateTaskPayload, { count: number } async (args, context) { if (!context.user) { throw new HttpError(401); } return context.entities.Task.updateMany({ where: { id: args.id, user: { id: context.user.id } }, data: { isDone: args.isDone }, }); };可以看到两个演进点一是通过context.user判断登录态并抛出HttpError(401)二是更新操作改用updateMany并把user放进where条件确保用户只能修改自己的任务。这说明 Action 的context参数同样是访问认证状态的统一入口——写操作、鉴权、数据隔离都在同一个函数内完成。小结本篇完整覆盖了 0.12 版教程《Modifying Data》的全部内容声明main.wasp中用action { fn, entities }注册写操作当前 TS spec 中等价于action(fn, { entities: [...] })见 main.wasp.ts实现src/actions中用context.entities.Entity.create/update写库TS 下用wasp/server/operations生成的CreateTask/UpdateTask类型获得全栈类型安全调用Action 在客户端直接await调用无需 Hook 包裹自动同步entities字段驱动客户端的实体级 Query 失效源码实现见 resources.js使所有 Query 与 Action 修改自动保持一致。完成这一节后你的 Todo 应用已具备创建与更新任务的能力下一步就是教程中提到的多用户支持。【免费下载链接】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),仅供参考