拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Wasp 教程(六):使用 Action 修改数据——创建与更新任务的全栈实践

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 官方教程的第六部分在上一章学会用 Query 读取数据库之后你将掌握与 Query 互补的Action机制——它是 Wasp 中创建、修改、删除数据的唯一入口。本文将以 Todo 应用为例带你完成声明 Action → 实现服务端逻辑 → 在 React 组件中调用的完整闭环并深入讲解 Wasp 基于 Entity 的自动查询失效机制最终实现一个新建任务 勾选完成的可持久化 Todo 应用。读完本文你将能够独立运用 Action 编写具备全栈类型安全的增改业务逻辑并理解 Wasp 为何能让前端数据自动保持新鲜。1. Actions 与 Queries分工明确的两类 Operation在 Wasp 中读写数据的核心抽象统称为Operation操作分为两类见>import { action, app, page, query, route } from wasp.sh/spec import { MainPage } from ./src/MainPage with { type: ref } import { getTasks } from ./src/queries with { type: ref } import { createTask } from ./src/actions with { type: ref } export default app({ // ... spec: [ route(RootRoute, /, page(MainPage)), query(getTasks, { entities: [Task] }), action(createTask, { entities: [Task] }), ], })这里action(createTask, { entities: [Task] })完成了两件事告诉 Wasp 存在一个名为createTask的 ActionAction 名由传入函数的导出名推导而来通过entities选项声明该 Action 会读写Task实体——这一声明是后面自动查询失效机制的基石详见第 5 节。Wasp 会根据这段声明自动生成同名函数服务端生成可调用的 Node.js 函数客户端生成可从wasp/client/operations导入的调用函数并在服务端自动挂载对应的 HTTP API 路由处理器参见 Actions 文档。这意味着你不需要手写任何 REST/GraphQL 接口代码。2.2 实现服务端 Action 函数接下来定义createTask的函数实现。官方推荐将其放在新文件src/actions.tsJavaScript 版为src/actions.js中import type { Task } from wasp/entities; import type { 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 }, }); };关键点解析CreateTask泛型类型这是 Wasp 根据main.wasp.ts中的 Action 声明自动生成的类型接受两个可选类型参数——InputAction 入参类型即args与OutputAction 返回值类型。这里声明为CreateTaskCreateTaskPayload, Task即接收{ description: string }返回Task。context.entities.Taskcontext.entities暴露了你声明在entities选项中的实体其本质是 Prisma 的 CRUD APIprisma.task因此可以直接调用.create()、.update()等方法。上文用的Task.create({ data: { description: args.description } })即 Prisma 的创建语法isDone字段会在数据库层用默认值false填充见 schema.prisma 中Task模型的isDone Boolean default(false)。关于类型注解给 Action 实现标注CreateTask/UpdateTask这类类型是可选的但强烈推荐。一旦标注客户端调用该 Action 时就能获得完全一致的入参/返回值类型推导即 Wasp 的全栈类型安全full-stack type safety。若省略两个类型参数TypeScript 会推断为最宽松的never入参与unknown返回值。文件位置自由实现函数放在src/actions.ts只是惯例并非限制。只要main.wasp.ts中的引用导入指向正确、且文件位于src目录内放在任何位置都可以。3. 在客户端调用 Action实现新建任务表单Query 需要useQuery这类 Hook 来驱动响应式更新而Action 不需要响应性因此可以直接调用无需包裹任何 Hook——剩下的就是常规 React 代码。3.1 编写 NewTaskForm 表单组件在src/MainPage.tsx中新增一个表单组件import type { SubmitEvent } from react; import type { Task } from wasp/entities; import { createTask, getTasks, useQuery, } from wasp/client/operations; // ... MainPage, TaskView, TasksList ... const NewTaskForm () { const handleSubmit async (event: SubmitEventHTMLFormElement) { 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 ); };createTask从wasp/client/operations导入——这是 Wasp 为客户端生成的调用函数内部通过自动生成的 HTTP API 与服务器通信将{ description }序列化后传给服务端实现作为第一个参数args。由于我们在服务端实现中标注了CreateTaskCreateTaskPayload, Task这里传入{ description }若与PickTask, description不匹配TypeScript 会直接报错。你可以试着把参数改错并阅读编辑器里的错误信息来验证。返回值是Task不过本示例暂未使用它。3.2 把表单挂载到页面将NewTaskForm渲染进MainPage让它出现在任务列表上方import type { SubmitEvent } from react; import type { Task } from wasp/entities; import { createTask, getTasks, useQuery } from wasp/client/operations; export const MainPage () { const { data: tasks, isLoading, error } useQuery(getTasks); return ( div NewTaskForm / {tasks TasksList tasks{tasks} /} {isLoading Loading...} {error Error: error} /div ); }; // ... TaskView, TasksList, NewTaskForm ...现在刷新页面输入 Build a Todo App in Wasp 并提交任务会出现在列表中刷新页面或换一个浏览器打开任务依然存在——因为任务是在服务端创建并写入数据库的而不是停留在前端内存里。下图展示了创建任务后的实际运行效果表单 任务列表4. 自动查询失效数据为何自动保持同步你可能注意到了创建新任务后任务列表自动显示了新任务而代码里并没有任何手动刷新列表的逻辑。这正是 Wasp 的**自动查询失效Automatic Query Invalidation**机制在起作用。4.1 基于 Entity 的失效规则回顾声明部分getTasks与createTask都通过entities选项声明使用Task实体。因此当createTask被执行时Wasp 推断getTasks所读取的数据可能已发生变化于是自动使其缓存失效并在后台重新拉取。用官方文档的原话概括开箱即用Wasp 会把你所有 Query 与任何通过 Action 发生的数据变更保持同步。Actions 文档 · Cache Invalidation从实现层面看Wasp 在客户端使用react-query管理 Query 缓存服务端在 Action 执行后依据其声明的实体集合批量使引用同一实体的 Query 缓存失效。这套机制避免了手写 refetch / 手动失效带来的复杂性与易错性。4.2 使用注意与已知边界该行为作为默认策略非常方便但在大型应用中可能造成不必要的重取并非每次写入都真正影响每个查询。需要知道的两个边界目前 Wasp 尚未提供**覆盖override**该自动失效行为的机制官方已有计划可参见仓库中的 issue 追踪自动失效仅对声明了entities的实体数据生效。若你有更精细的需求可以借助react-query自身的失效 API或使用 Wasp 的useActionHook 配合**乐观更新optimistic updates**手动控制缓存状态——完整 API 见 Actions 文档 · useAction Hook。5. 第二个 ActionupdateTask 标记任务完成只有新建任务还不够Todo 应用必须能标记已完成。请先尝试独立完成创建一个名为updateTask的 Action接收任务的id与其isDone状态。参考实现如下。5.1 在 main.wasp.ts 中声明import { action, app, page, query, route } from wasp.sh/spec import { MainPage } from ./src/MainPage with { type: ref } import { getTasks } from ./src/queries with { type: ref } import { createTask, updateTask } from ./src/actions with { type: ref } export default app({ // ... spec: [ // ... existing routes and queries action(createTask, { entities: [Task] }), action(updateTask, { entities: [Task] }), ], })注意updateTask与createTask一样声明entities: [Task]这保证勾选完成后getTasks的缓存同样会被自动失效、列表即时刷新。5.2 实现服务端逻辑import type { Task } from wasp/entities; import type { 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, }, }); };UpdateTask同样是 Wasp 自动生成的泛型类型这里将其输入定义为PickTask, id | isDone并使用 Prisma 的Task.update({ where: { id }, data: { isDone } })更新目标记录。5.3 从 React 组件中调用在TaskView组件中监听复选框的onChange事件并调用updateTaskimport type { ChangeEvent, SubmitEvent } from react; import type { 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 ); }; // ... TasksList, NewTaskForm ...与createTask一样updateTask也是直接调用无需 Hook。类型安全同样生效若传入的载荷与UpdateTaskPayload不符TypeScript 会立刻提示。勾选后自动查询失效会让列表数据即时反映新状态。6. 对照仓库完整示例中的进阶细节教程到这里已经完成了一个可用的 Todo 应用。但仓库中的 TodoAppTs 示例是教程的最终状态它还引入了用户认证auth从中可以学到两个在生产环境极为重要的进阶写法。6.1 在 Action 中访问当前用户并做鉴权示例的 src/actions.ts 在函数体开头通过context.user判断登录态未登录直接抛出HttpError(401)import type { Task } from wasp/entities; import { HttpError } from wasp/server; import type { CreateTask, UpdateTask } from wasp/server/operations; type CreateTaskPayload PickTask, description; 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 } }, }, }); };当应用配置了auth见 main.wasp.ts 中的auth: { userEntity: User, methods: { usernameAndPassword: {} } }Wasp 会在后台完成登录用户的认证并把用户信息注入 Action 的context.user。这里的data.user: { connect: { id: context.user.id } }将新任务通过 Prisma 外键关联到当前用户。关于错误处理出于安全考虑Action 实现中抛出的普通异常会以 HTTP 500 返回给客户端且不携带细节避免泄露敏感信息需要向客户端传递可读错误时应显式抛出HttpError详见 Actions 文档 · Error Handling。6.2 用 updateMany 做本人数据约束示例中的updateTask没有使用Task.update而是改用Task.updateMany并在where中同时限定id和user.idexport const updateTask: UpdateTask UpdateTaskPayload, { 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 }, }); };这保证用户只能更新属于自己的任务返回{ count: number }表示实际更新的记录数是多用户场景下防止越权修改的经典写法。6.3 端到端测试验证仓库还为该应用提供了 Playwright 端到端测试 simple.spec.ts完整覆盖本章的全部功能路径const randomTask New Task ${Math.random().toString(36).substring(7)}; await page.locator(input[namedescription]).fill(randomTask); await page.locator(input[typesubmit]).click(); await expect(page.locator(body)).toContainText(randomTask); await page.locator(input[typecheckbox]).click(); await page.reload(); await expect(page.locator(input[typecheckbox])).toBeChecked();这段测试依次验证了通过表单创建任务 → 新任务出现在列表 → 勾选复选框完成标记 →刷新页面后勾选状态依然保留证明数据确实持久化到了数据库。它也顺带验证了第 3 节刷新后任务仍在的结论。7. 继续深入Action 的完整 API 能力本章教程覆盖了 Action 最核心的用法而 Wasp 官方文档 Actions 还提供了更多可组合的能力值得按需查阅在服务端调用 Action从wasp/server/operations导入同名函数并显式传入{ user }上下文客户端调用时由 Wasp 自动注入认证信息。useActionHook 与乐观更新虽然 Action 可以直接调用但useAction(actionFn, { optimisticUpdates: [...] })可以在等待服务器响应的同时用updateQuery纯函数先行更新查询缓存获得即时 UI 反馈。queryCacheKey若需要绕过 Wasp 的抽象、直接使用react-query底层 API如useMutation可以通过getTasks.queryCacheKey拿到查询缓存键。Action 规格 API 参考action声明函数的全部可选参数见 spec 文档中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),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门