ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Wasp Actions 完全指南:声明式修改数据的全栈操作(附缓存失效与乐观更新实战)

Wasp Actions 完全指南:声明式修改数据的全栈操作(附缓存失效与乐观更新实战) 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/waspActions 是 Wasp 中负责修改与新增数据的操作Operation与只读的 Queries 相辅相成共同维护前端数据缓存的实时性。本指南以 Wasp 0.13 版本的官方文档web/versioned_docs/version-0.13/data-model/operations/actions.md为骨架结合仓库中 SDK 模板源码完整讲解 Action 的声明、实现、调用、错误处理、Entity 注入、自动缓存失效以及useAction钩子与乐观更新帮助你用最少代码获得端到端类型安全的写操作能力。Actions 是什么Actions 与 Queries 非常相似核心区别在于用途Actions 被设计用来修改和添加数据而 Queries 只用于读取数据。典型 Action 场景包括给博客文章添加评论、给视频点赞、更新商品价格等。从 Operations 总览文档 的定义看Wasp 中的 Operation 分为两类Queries 负责读Actions 负责写更新已有记录或创建新记录。两者协作才能让前端数据始终保持最新。Wasp 官方文档给出的建议是如果已熟悉 Queries可以跳过大部分雷同的内容直接阅读「Queries 与 Actions 的差异」和 API Reference。使用 Actions 的两个步骤Action 在 Wasp 中声明、在 Node.js 中实现。Wasp 在服务端上下文中运行 Action同时生成代码让你在客户端或服务端的任何位置用完全相同的接口调用它。这意味着你不需要为 Action 手动搭建 HTTP API自己处理服务端的请求路由操心客户端响应处理与缓存管理。你只需要专注 Action 内部的业务逻辑。创建一个 Action 只需两步在 Wasp 文件中用action声明它实现该 Action 的 Node.js 功能。两步完成后即可在代码任意位置使用该 Action。声明 Actions在 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 Reference。Wasp Action 的名称与其实现导出名不必完全一致但为免混淆官方示例保持二者相同。声明后 Wasp 自动生成的代码声明一个 Wasp Action 后会发生两件重要的事Wasp 会生成一个与服务端同名的 Node.js 函数。Wasp 会生成一个与服务端同名的客户端 JavaScript 函数例如markTaskAsDone。该函数接收一个可选参数——一个包含任意可序列化数据的对象供 Action 内部使用。Wasp 会把这个对象通过网络发送出去并作为第一个位置参数传入 Action 的实现。这个抽象之所以成立是因为 Wasp 在服务端生成了一个 HTTP API 路由处理器在底层调用 Action 的 Node.js 实现。生成这两个函数保证了整个应用客户端与服务端拥有统一的调用接口。从 SDK 模板源码waspc/data/Generator/templates/sdk/wasp/client/operations/actions/core.ts可以看到客户端 Action 由createAction工厂函数生成它先根据相对路由构造actionRoute内部internalAction会先registerActionInProgress登记进行中的乐观更新然后通过callOperation(actionRoute, args)发起 RPC 调用最后在finally中registerActionDone(entitiesUsed, ...)触发基于 Entity 的缓存失效——这正是后面「缓存失效」一节的运行时基础。superjson超越 JSON 的载荷Wasp 在底层使用 superjson见 _superjson-note.md因此你不必把自己限制在纯 JSON 载荷里。你可以发送和接收任意 superjson 兼容的载荷例如Date、Set、List、循环引用等由 Wasp 负责序列化与反序列化。在 TypeScript 下只要用自动生成的正确类型标注 Action编译器就能保证载荷合法即 Wasp 知道如何序列化/反序列化它们。在 Node 中实现 Actions声明时我们让 Wasp 去src/actions.{js,ts}中寻找实现因此需要从该文件导出。下面是用内存数组模拟数据库的实现JavaScript 版本src/actions.js 同款示例// 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 版本利用自动生成的泛型类型实现全栈类型安全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 } export const markTaskAsDone: MarkTaskAsDonePickTask, id, void ( args ) { const task tasks.find((task) task.id args.id) if (!task) { return } task.isDone true }Wasp 会根据 Wasp 文件中的声明自动生成CreateTask和MarkTaskAsDone这两个泛型类型CreateTask对应createTask的声明MarkTaskAsDone对应markTaskAsDone的声明用于标注 Action 的输入与输出类型createTask期望收到{ description: string }类型的对象返回新建的任务Task类型对象。markTaskAsDone期望收到{ id: number }类型的对象不返回任何内容返回类型为void。标注 Action 是可选的但强烈推荐——它带来了端到端的类型安全full-stack type safety在客户端调用 Action 时即可享受类型推断与校验。使用 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 // TypeScript 自动推断返回值并做类型检查 const newTask await createTask({ description: Keep learning TypeScript }) await markTaskAsDone({ id: 1 })在客户端使用 Action 时最常见的场景是在组件内调用。由于 Action 不需要响应式reactive直接调用即可Wasp 还提供了useAction钩子为 Action 增加额外行为如乐观更新详见 API Reference。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 ) }错误处理出于安全考虑Action 的 Node.js 实现中抛出的所有异常都会以 HTTP500状态码发送给客户端并且剥离所有其他细节。默认隐藏错误细节是为了避免敏感信息通过网络的意外泄露。如果你确实希望把额外的错误信息传给客户端可以在实现中构造并抛出合适的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 ) }从 SDK 模板源码waspc/data/Generator/templates/sdk/wasp/server/HttpError.ts可以看到HttpError的实现约束状态码必须是[400, 600)区间的整数否则构造时直接抛出ErrorstatusCode、data作为公开属性暴露方便客户端读取。这解释了为什么只有显式抛出HttpError时错误信息才能安全、可控地传递到客户端。在 Actions 中使用 Entities大多数情况下Action 中使用的资源是 Entities。要使用某个 Entity把它加进 Wasp 文件中的action声明即可action createTask { fn: import { createTask } from src/actions.js, entities: [Task] } action markTaskAsDone { fn: import { markTaskAsDone } from src/actions.js, entities: [Task] }Wasp 会把指定的 Entity 注入 Action 的context参数让你获得该 Entity 的 Prisma API。同时Wasp 会根据每个 Action/Query 使用的 Entity 来失效前端 Query 缓存详见「缓存失效」。JavaScript 实现// 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 }, }) }context.entities.Task这个对象暴露的是 Prisma 的 CRUD API 中prisma.task的能力create、update、delete、findUnique等这意味着你可以把 Action 内的数据操作完全交给 Prisma 的类型安全客户端。缓存失效Cache Invalidation管理 Web 应用状态最棘手的问题之一是确保 Query 返回的数据始终是最新的。Wasp 使用 react-query 管理 Query因此当数据变旧时必须使 Query更准确地说是 react-query 管理的缓存结果失效。你可以通过 react-query 提供的多种机制手动失效缓存例如 refetch、直接 invalidate。但手动失效很快会变得复杂且易错因此 Wasp 提供了更快、更有效的开箱即用方案基于 Entity 的自动 Query 缓存失效。由于 Action 经常且多数情况下应该修改状态而 Query 读取状态Wasp 会在某个使用了相同 Entity 的 Action 被执行后自动失效对应 Query 的缓存。例如如果 ActioncreateTask和 QuerygetTasks都使用了 EntityTask那么执行createTask可能导致getTasks的缓存结果过期Wasp 会立即失效它促使getTasks从服务端重新拉取数据并更新。实践中这意味着你无需思考缓存失效Wasp 自动让 Query 保持新鲜。其运行时机制可以从客户端 SDK 模板源码得到印证waspc/data/Generator/templates/sdk/wasp/client/operations/internal/resources.jsWasp 维护一张resourceToQueryCacheKeys映射表resource 名 → 使用该 resource 的 query 缓存键集合addResourcesUsedByQuery在 Query 注册时记录依赖关系Action 完成后registerActionDone调用invalidateQueriesUsing(resources)对所有使用相同 Entity 的缓存键执行queryClient.invalidateQueries(queryCacheKey)从而触发重新拉取。另一方面这种自动缓存失效也可能有些浪费某些更新并非必需并且只对 Entity 生效。如果这是问题你可以暂时使用 react-query 提供的机制并期待 Wasp 未来以更优雅的方式直接支持这些用例。若希望在执行 Action 后**乐观地optimistically**设置缓存值可以使用乐观更新方案通过 Wasp 的useAction钩子配置。这是目前 Wasp 原生支持的唯一手动缓存失效机制其余场景可以依赖 react-query。Queries 与 Actions 的差异Actions 和 Queries 是 Wasp 中两个紧密相关的概念Wasp 对它们的处理不同各自代表不同的含义。关键差异如下写 vs 读Action 可以且通常应当修改服务端状态而 Query 只被允许读取。Wasp 执行缓存失效时依赖你遵守这一约定因此严格遵守至关重要。响应式Action 不需要响应式可以直接调用。不过 Wasp 提供了useActionReact 钩子为 Action 添加额外行为如乐观更新。声明语法Wasp 中的action声明与query声明几乎完全一致唯一区别在于声明名称。API Reference在 Wasp 中声明 Actionaction声明支持以下字段字段必填说明fn: ExtImport✅Action 的 Node.js 实现的 import 语句entities: [Entity]❌希望在 Action 内使用的 Entity 列表使用方式见「在 Actions 中使用 Entities」示例action createFoo { fn: import { createFoo } from src/actions.js entities: [Foo] }声明后即可在代码任意位置服务端或客户端导入使用import { createFoo } from wasp/client/operationsTypeScript 下还可以在服务端使用类型导入// 在客户端使用 import { createFoo } from wasp/client/operations // 在服务端使用 import { createFoo } from wasp/server/operations // 服务端的类型导入 import { type CreateFoo } from wasp/server/operations实现 ActionsAction 的实现是一个接收两个参数的 Node.js 函数需要await时可以是async函数。两个参数都是位置参数参数名可自定官方约定为args和contextargs类型取决于 Action一个对象包含调用 Action 时传入的数据例如过滤条件。调用方式参见「使用 Actions」。context类型取决于 Action由Wasp 注入的附加上下文对象包含用户会话信息以及 Entity 信息。Entity 的使用见「在 Actions 中使用 Entities」user对象的使用见 auth 文档的 context.user 小节。TypeScript 生成的类型声明 Action 后Wasp 会生成一个可用的泛型类型。对声明为createSomething的 Action生成的类型叫CreateSomethingimport { type CreateSomething } from wasp/server/operations它接受两个可选的类型参数Inputargs对象即 Action 输入载荷的类型默认值为never。OutputAction 返回值即 Action 输出载荷的类型默认值为unknown。默认值的选择是为了让类型签名尽可能宽松。如果不需要 Action 接收/返回任何内容用void作为类型参数即可。实现示例action createFoo { fn: import { createFoo } from src/actions.js entities: [Foo] }Wasp 期望从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 实际返回值类型一致。useAction 钩子与乐观更新在阅读本章前请先理解 Queries 和「缓存失效」的工作原理。在组件中使用 Action 时可以用useAction钩子增强它们。该钩子随 Wasp 内置用于装饰decoratingWasp Action——它返回一个 API 与原 Action 完全一致、但底层做了额外事情取决于你的配置的函数。useAction接收两个参数actionFn必填想要增强的 Wasp Action即 Wasp 根据 action 声明生成的客户端 Action 函数。actionOptions一个配置对象用于给 Action 添加额外特性。该参数技术上可选但不提供它就没有使用useAction的意义与直接调用 Action 无异。支持以下字段optimisticUpdates一个对象数组每个对象定义一个要在 Query 缓存上执行的乐观更新。定义乐观更新需要指定两个属性getQuerySpecifier必填一个函数返回 Query 说明符specifier即用来定位要更新 Query 的值。Query 说明符是一个数组指定 query 函数与参数。例如要对useQuery(fetchFilteredTasks, { isDone: true })使用的 Query 做乐观更新getQuerySpecifier需要返回数组[fetchFilteredTasks, { isDone: true }]。Wasp 会把传入装饰后 Action 的参数转发给这个函数即可以利用新增/修改项的属性来定位 Query。updateQuery必填执行乐观更新的函数返回缓存的期望状态。Wasp 会以如下参数调用它item—— 传入装饰后 Action 的参数。oldData—— 由说明符定位的 Query 当前缓存值。:::cautionupdateQuery必须是纯函数返回getQuerySpecifier定位的期望缓存值且绝不能有副作用。同时只更新由你的 Action引发该乐观更新的 Action影响的 Query 缓存Wasp 目前还无法校验这一点。最后updateQuery的实现应能在任何oldData状态下正确工作例如不要依赖数组位置。如果乐观更新期间还需要做别的事可以直接使用 react-query 的底层 API见「高级用法」。 :::下面演示如何为把任务isDone状态切换为完成的 ActionmarkTaskAsDone配置乐观更新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 }); 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;从 SDK 模板源码waspc/data/Generator/templates/sdk/wasp/client/operations/hooks.ts可以看清useAction的底层实现它内部调用 react-query 的useMutation但刻意隐藏了 react-query 的额外 mutation 特性如isLoading、onSuccess/onError回调、同步mutate只暴露一个 API 与原 Action 一致的异步函数。这避免了 API 混乱也把 Wasp 有主见的 API 与 react-query 的底层高级 API 清晰区分开。乐观更新在底层被翻译为 react-query 的onMutate/onErroronMutate先取消正在进行的 refetch避免覆盖乐观更新、快照旧缓存并setQueryData写入新值出错时onError会把快照恢复回去hooks.ts。高级用法useAction钩子目前只支持指定乐观更新未来版本会有更多特性。Wasp 的乐观更新 API 刻意保持小巧只专注于更新 Query 缓存因为这是最常见场景。如果你需要更多选项或更高控制级别的 API可以不使用 Wasp 的useAction改用 react-query 的useMutation钩子直接操作它的底层 API。如果决定直接使用 react-query 的 API你需要拿到 Query 缓存键。Wasp 内部使用这个键但对其做了抽象不过你可以通过任意 Query 的queryCacheKey属性轻松获取import { getTasks } from wasp/client/operations const queryKey getTasks.queryCacheKey小结Actions 是 Wasp 数据写入侧的基石能力action声明 Node 实现即可获得统一的前后端调用接口通过entities字段注入 Prisma API 并获得基于 Entity 的自动缓存失效用HttpError控制错误透传用useAction钩子实现乐观更新。结合 SDK 模板源码actions/core.ts、hooks.ts、resources.js你可以完整理解从声明、生成、RPC 调用到缓存失效的整条链路从而在真实项目中更自信地设计数据变更逻辑。【免费下载链接】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),仅供参考
返回列表