十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Wasp Operations 全栈数据操作指南:Query 读、Action 写与缓存自动失效机制

Wasp Operations 全栈数据操作指南:Query 读、Action 写与缓存自动失效机制 Wasp Operations 全栈数据操作指南Query 读、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/waspOperations 是 Wasp 框架中承接数据模型Entity之上所有数据读写的统一抽象它把声明式配置 Node.js 实现 前后端自动代码生成 HTTP 路由 缓存管理打包成一套开箱即用的方案。读完本文你将掌握如何用query与action两种 spec 声明数据操作、如何实现并调用它们、如何利用useQuery/useAction实现响应式与乐观更新并深入理解 Wasp 基于 Entity 的 Query 缓存自动失效机制及其源码原理。OperationsEntity 数据之上的两类操作入口Wasp 的官方文档Operations 总览开宗明义Entities 用于定义应用的数据模型和关系而 Operations 则负责处理操作这些数据。数据模型解决有哪些数据、数据之间是什么关系Operations 解决如何读写这些数据。Operations 分为两种名称即用途Operation 类型用途典型场景Query读取数据read获取一篇博客文章的所有评论、喜欢某个视频的用户列表、根据 ID 查询单个产品信息Action修改数据write给博客文章添加评论、点赞视频、更新产品价格Action 既可以更新既有记录也可以创建新记录。两者在 API 形态上高度相似但在语义、约束与底层处理上被 Wasp 区别对待——这一点在本文的缓存失效机制与核心区别两节会详细展开。值得强调的是在 Wasp 中你不必为每个 Operation 手写 HTTP API、管理服务端请求处理、处理客户端响应与缓存。只需声明 实现两步Wasp 编译器会自动生成服务端路由处理器与客户端调用函数让你从应用代码的任何位置客户端或服务端以同一套接口调用。第一步在 Wasp 规范中声明 Query要创建一个 Query第一步是在 Wasp 主文件中用queryspec 声明它。以官方文档的任务列表示例为例Queries 文档声明两个 Query——一个获取全部任务一个按过滤条件如是否完成获取任务import { app, query } from wasp.sh/spec import { getAllTasks, getFilteredTasks } from ./src/queries with { type: ref } export default app({ // ... spec: [ query(getAllTasks), query(getFilteredTasks), ], })声明之后Wasp 会从传入query的函数名推导出 Query 的名字。例如query(getFilteredTasks)就创建了一个名为getFilteredTasks的 Query。紧接着会发生两件重要的事生成服务端 Node.js 函数同名承载实际的业务逻辑生成客户端 JavaScript 函数同名例如getFilteredTasks它接收一个可选参数——一个包含任意可序列化数据的对象。Wasp 会把这个对象通过网络发送并作为第一个位置参数传入 Query 的实现。这套抽象之所以成立是因为 Wasp 在服务端自动生成了一个 HTTP API 路由处理器它内部调用 Query 的 Node.js 实现。两份同名函数保证了整个应用客户端与服务端拥有完全一致的调用接口。关于queryspec 支持的全部选项可以查看文档中的 API Reference 以及 wasp.sh/spec 的 query 函数 API 说明该链接对应仓库中已发布的 API 文档目录。注意上面示例中 import 的实现函数getAllTasks等此时还不存在。这正是 Wasp 推荐的工作顺序——先写高层的声明式 spec再实现具体的 Node.js 逻辑。实现 Query 的 Node.js 函数声明完成后接下来实现它。上面告知 Wasp 去src/queries.{js,ts}中寻找实现因此需要在该文件里具名导出对应函数// our database 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 }, ] // You dont need to use the arguments if you dont need them export const getAllTasks () { return tasks } // The args object is something sent by the caller (most often from the client) export const getFilteredTasks (args) { const { isDone } args return tasks.filter((task) task.isDone isDone) }args 与 context两个位置参数Query 的实现是一个 Node.js 函数接收两个位置参数可以是async函数。由于是位置参数你可以随意命名文档惯例用args和contextargs类型取决于 Query调用方传入的数据对象如过滤条件。调用时以对象形式传给 QueryWasp 会将其作为第一个位置参数注入。若 Query 不需要入参可以完全忽略该参数。context类型取决于 QueryWasp 注入的上下文对象包含用户会话信息与实体Entity信息。是否包含user对象取决于该 Query 是否启用 auth是否包含entities取决于 spec 中配置了哪些 Entity。详见后文在 Operation 中使用 Entity一节以及 auth 文档中关于 context.user 的说明。TypeScript 泛型类型GetXxx使用 TypeScript 时Wasp 会根据 spec 自动生成以 Operation 名命名的泛型类型。声明getAllTasks和getFilteredTasks后就可以在实现中引用生成的GetAllTasks、GetFilteredTasks类型import { type GetAllTasks, type GetFilteredTasks } from wasp/server/operations type Task { id: number description: string isDone: boolean } // our database const tasks: Task[] [ { 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 getAllTasks: GetAllTasksvoid, Task[] () { return tasks } export const getFilteredTasks: GetFilteredTasks PickTask, isDone, Task[] (args) { const { isDone } args return tasks.filter((task) task.isDone isDone) }生成的泛型类型接收两个可选类型参数InputQuery 函数收到的入参payload类型默认neverOutputQuery 函数的返回类型默认unknown。默认值特意选得足够宽松。若希望 Query 不接收/不返回任何内容显式使用void作为类型参数即可。getAllTasks的输入类型为void、输出类型为Task[]getFilteredTasks期望收到{ isDone: boolean }类型的对象。指定类型参数后可获得实现内部对入参与返回值的类型支持以及贯穿全栈的类型安全full-stack type safety。用 satisfies 自动推断返回类型如果不想手动书写返回类型可以用satisfies关键字让 TypeScript 自动推断const getFoo (async (_args, context) { const foos await context.entities.Foo.findMany() return { foos, message: Here are some foos!, queriedAt: new Date(), } }) satisfies GetFooTypeScript 由此得知context的正确类型以及 Query 返回类型为{ foos: Foo[], message: string, queriedAt: Date }。如果连 context 都不需要甚至可以完全不标注类型const getFoo () ({ name: Foo, date: new Date() })在客户端与服务端调用 Query客户端调用在客户端从wasp/client/operations导入并直接调用import { getAllTasks, getFilteredTasks } from wasp/client/operations const allTasks await getAllTasks() const doneTasks await getFilteredTasks({ isDone: true })调用方式与 Query 是否启用认证无关——Wasp 会在后台自动完成已登录用户的认证。在 TypeScript 下客户端会自动获得返回值类型推断与 payload 的类型检查自动全栈类型安全只需在服务端定义中指定 Query 的类型客户端代码即可自动获知 API payload 类型。服务端调用服务端调用与客户端几乎一致只有两点不同从wasp/server/operations而非wasp/client/operations导入对需要认证的 Query必须传入带有user字段的context对象。注意context的其他部分如 Entities无需手动传入Wasp 会自动注入。import { getAllTasks, getFilteredTasks } from wasp/server/operations const user // 获取 AuthUser 对象例如来自某个 Operation 的 context.user const allTasks await getAllTasks({ user }) const doneTasks await getFilteredTasks({ isDone: true }, { user })useQuery Hook让 Query 具备响应式能力在客户端使用 Query 时可以通过useQueryhook 让查询结果具备响应式reactive能力。该 hook 随 Wasp 内置是 react-query 的useQuery的一层薄封装唯一区别在于无需手动提供缓存 key——Wasp 在内部自动处理import React from react import { useQuery, getAllTasks, getFilteredTasks } from wasp/client/operations const MainPage () { const { data: allTasks, error: error1 } useQuery(getAllTasks) const { data: doneTasks, error: error2 } useQuery(getFilteredTasks, { isDone: true, }) if (error1 ! null || error2 ! null) { return divThere was an error/div } return ( div h2All Tasks/h2 {allTasks allTasks.length 0 ? allTasks.map((task) Task key{task.id} {...task} /) : No tasks} {/* ... */} /div ) }在 TypeScript 中无需手动标注 Query 的返回值类型Wasp 会根据服务端实现自动推断——这正是全栈类型安全的体现客户端的类型永远与服务端一致。useQuery接受三个参数queryFn必填Wasp 根据queryspec 生成的客户端查询函数queryFnArgs希望传入 Query 的参数对象Query 的 Node.js 实现会将其作为第一个位置参数接收optionsreact-query 的 options 对象用于调整该 Query 的默认行为如需修改全局默认值可以在 客户端 setup 函数 中配置。Action修改数据的操作Action 与 Query 高度相似区别在于Action 专门用于修改和新增数据。官方文档Actions 文档给出的例子包括给博客添加评论、点赞视频、更新产品价格。创建 Action 同样只需两步用actionspec 声明 实现 Node.js 逻辑。声明与实现 Actionimport { action, app } from wasp.sh/spec import { createTask, markTaskAsDone } from ./src/actions with { type: ref } export default app({ // ... spec: [ action(createTask), action(markTaskAsDone), ], })action(markTaskAsDone)会创建名为markTaskAsDone的 Action并同样生成服务端 Node.js 函数与客户端同名调用函数。实现如下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 } 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 export const createTask: CreateTaskPickTask, description, Task ( args ) { // 实现逻辑 } export const markTaskAsDone: MarkTaskAsDonePickTask, id, void ( args ) { // 实现逻辑 }调用 Action客户端调用与 Query 相同从wasp/client/operations导入import { createTask, markTaskAsDone } from wasp/client/operations const newTask await createTask({ description: Learn TypeScript }) await markTaskAsDone({ id: 1 })由于 Action 不依赖响应式能力在组件中直接调用即可无需 hook。官方文档展示了典型的组件内用法——先用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 同样从wasp/server/operations导入并对认证 Action 传入{ user }上下文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 })错误处理HttpError 与信息脱敏出于安全考虑Query/Action 实现中抛出的所有异常默认都会以 HTTP 500 状态码返回给客户端且移除一切其他细节。隐藏错误细节可以避免敏感信息意外泄露到网络上。如果确实需要向客户端传递额外错误信息可以在实现中构造并抛出HttpErrorimport { HttpError } from wasp/server export const getAllTasks async (args, context) { throw new HttpError( 403, // status code You cant do this!, // message { foo: bar } // data ) }当状态码为4xx时客户端会收到包含对应message和data字段的响应对象并重新抛出携带这些字段的错误对于其他状态码服务端不会转发这些字段以防止信息泄漏。在 Operation 中使用 Entitycontext.entities大多数情况下Operation 操作的数据来自 Wasp 的 Entities。要在 Operation 中使用 Entity需要将其加入 specimport { app, query } from wasp.sh/spec import { getAllTasks, getFilteredTasks } from ./src/queries with { type: ref } export default app({ // ... spec: [ query(getAllTasks, { entities: [Task] }), query(getFilteredTasks, { entities: [Task] }), ], })Wasp 会把指定的 Entity 注入到 Operation 的context参数中从而在实现里直接使用该 Entity 的 Prisma APIexport const getAllTasks async (args, context) { return context.entities.Task.findMany({}) } export const getFilteredTasks async (args, context) { return context.entities.Task.findMany({ where: { isDone: args.isDone }, }) }context.entities.Task暴露的就是 Prisma 的 CRUD API如findMany、create、update等。Action 中的用法完全一致例如export const createTask async (args, context) { const newTask await context.entities.Task.create({ data: { description: args.description, isDone: false, }, }) return newTask }基于 Entity 的 Query 缓存自动失效机制管理 Web 应用状态最棘手的问题之一是保证 Query 返回的数据始终最新。Wasp 使用 react-query 管理 Query因此必须在其缓存变旧时及时失效invalidate。虽然可以手动通过 react-query 提供的机制refetch、直接失效处理但手动失效很快会变得复杂且易错。Wasp 提供了一套更高效的开箱即用方案基于 Entity 的 Query 缓存自动失效automatic Entity-based Query cache invalidation。核心规则是每当一个使用了某 Entity 的 Action 被执行所有使用了同一个 Entity 的 Query 的缓存都会被失效。例如若 ActioncreateTask和 QuerygetTasks都使用了 EntityTask那么执行createTask可能使getTasks的缓存结果过期。Wasp 会自动失效该缓存触发getTasks从服务端重新拉取数据。这让你无需思考缓存失效问题即可保持 Query 新鲜。这种机制的代价是某些失效可能是不必要的略显浪费并且它只对 Entity 生效。如果这成为问题可以退回到 react-query 提供的手动机制。另外如果希望在执行 Action 后乐观地设置缓存值可以使用 Wasp 的useActionhook 进行乐观更新——这是目前 Wasp 原生支持的唯一手动缓存管理机制。源码视角resourceToQueryCacheKeys 与 invalidateQueriesUsing这套自动失效机制的底层实现位于 waspc/data/Generator/templates/sdk/wasp/client/operations/internal/resources.js。从源码可以清晰看到它的工作原理模块维护一个resourceToQueryCacheKeysMap资源名 → 使用该资源的 Query 缓存 key 集合。addResourcesUsedByQuery第 1928 行在生成 Query 时被调用把该 Query 的缓存 key 登记到它使用的每个资源名下当 Action 执行完成时registerActionDone(resources, optimisticUpdateTuples)第 3639 行会先移除乐观更新处理器再调用invalidateQueriesUsing(resources)invalidateQueriesUsing第 6473 行通过getQueriesUsingResources找到所有使用这些资源的 Query 缓存 key逐一调用queryClient.invalidateQueries(queryCacheKey)触发重新拉取。而在 waspc/data/Generator/templates/sdk/wasp/client/operations/queries/core.ts 中createQuery第 2955 行用 Operation 的相对路径构造queryCacheKeyconst queryCacheKey [relativeQueryPath]并生成路由buildAndRegisterQuery第 5870 行则在 Query 对象上挂载queryCacheKey、route元数据并调用addResourcesUsedByQuery完成资源登记。这就是Entity → Query映射关系的建立过程。useAction 与乐观更新useActionhook 用于装饰decorateWasp Action它返回一个 API 与原始 Action 完全一致的函数但在底层附带额外行为取决于配置。目前它只支持乐观更新optimistic updates。乐观更新的配置结构useAction接受两个参数actionFn必填要装饰的 Wasp Action由 spec 生成的客户端 Action 函数actionOptions配置附加特性的对象目前支持optimisticUpdates字段——一个对象数组每个对象定义一次针对 Query 缓存的乐观更新包含两个必填属性getQuerySpecifier返回 Query specifier 的函数。Query specifier 是一个数组指明要更新的 Query 函数及其参数。例如要对useQuery(fetchFilteredTasks, { isDone: true })使用的 Query 做乐观更新该函数需返回[fetchFilteredTasks, { isDone: true }]。Wasp 会把传入装饰后 Action 的参数转发给此函数因此可以利用新增/修改条目的属性来定位 QueryupdateQuery执行乐观更新的函数返回缓存的期望状态。Wasp 会以item传入装饰后 Action 的参数和oldData该 Query 当前的缓存值调用它。注意updateQuery必须是纯函数只能返回getQuerySpecifier定位到的期望缓存值绝不能产生副作用。同时只应更新确实受该 Action 影响的 Query 缓存并且实现应不依赖oldData的具体状态例如不要依赖数组下标。来看官方文档中把任务标记为完成的完整示例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 pstrongDescription: /strong{description}/p pstrongIs done: /strong{isDone ? Yes : No}/p {isDone || ( button onClick{() markTaskAsDoneOptimistically({ id })} Mark as done. /button )} /div ) } export default TaskPage点击按钮后界面会立即把任务显示为已完成乐观更新缓存随后后台执行真实的 Action最终由缓存失效机制保证数据与服务端一致。源码视角useAction 的实现useAction的实现位于 waspc/data/Generator/templates/sdk/wasp/client/operations/hooks.ts。从源码第 4984 行可以看到其设计意图它内部使用 react-query 的useMutation包装 Action但刻意隐藏了isLoading、onSuccess、onError回调、同步mutate等额外特性只暴露一个 API 与原始 Action 一致的异步函数mutation.mutateAsync。这样既避免了 API 混乱也把 Wasp 的定制化 API 与 react-query 的低层高级 API 清晰区分开——需要更多控制时可以直接使用 react-query乐观更新的核心逻辑在makeRqOptimisticUpdateOptions第 243306 行onMutate中先cancelQueries取消进行中的 refetch快照当前缓存值再调用queryClient.setQueryData(queryKey, updateQuery)写入期望的缓存状态onError中则把快照的旧数据回滚回去setQueryData(queryKey, data)。translateToInternalDefinition第 173195 行负责把公开的getQuerySpecifier/updateQuery结构翻译为内部基于 queryKey 的定义并校验两个字段必须是函数。高级用法queryCacheKey 与 react-query 底层 API如果 Wasp 的乐观更新 API 满足不了需求可以直接使用 react-query 的useMutation及其低层 API。由于 Wasp 内部使用缓存 key 但对外隐藏你可以通过任何 Query 上的queryCacheKey属性拿到它import { getTasks } from wasp/client/operations const queryKey getTasks.queryCacheKey这一元数据在 waspc/data/Generator/templates/sdk/wasp/client/operations/rpc.ts 中定义QueryMetadata类型第 4245 行包含queryCacheKey: string[]与route: Route两个字段Query 对象是可调用函数 × 元数据的交集类型。同时该文件还定义了从前端调用到后端实现的类型推导逻辑OperationRpcFor第 5763 行当后端操作不带参数时客户端函数呈现为无参形态() PromiseOutput当输入为void时同样如此否则为(args: Input) PromiseOutput——这就是全栈类型安全在类型层面上的实现基础。Query 与 Action 的核心区别虽然两者 API 几乎一致但 Wasp 对它们的语义处理截然不同文档原文见此读写语义Action 可以且通常应当修改服务端状态Query 只允许读取。Wasp 在执行缓存失效时依赖你遵守这一约定因此务必遵循响应式Action 无需响应式可以直接调用Wasp 提供useActionhook 为其附加额外行为如乐观更新spec 形态actionspec 与queryspec 几乎完全一致唯一区别在于 spec 名称。此外Action 与 Query 的 TypeScript 类型、调用接口、错误处理、Entity 注入方式也完全对称客户端从wasp/client/operations导入、服务端从wasp/server/operations导入生成的泛型类型GetXxx/CreateXxx均接受可选的Input/Output两个类型参数默认值分别为never/unknown。实战案例TodoApp 中的完整 Operation 用法仓库中的教程示例应用 examples/tutorials/TodoApp 提供了一个真实可运行的全栈示例其 Operations 配置如下import { action, app, page, query, route } from wasp.sh/spec import { createTask, updateTask } from ./src/actions with { type: ref } import { getTasks } from ./src/queries with { type: ref } export default app({ // ... auth: { userEntity: User, methods: { usernameAndPassword: {} }, onAuthFailedRedirectTo: /login, }, spec: [ route(RootRoute, /, page(MainPage, { authRequired: true })), // ... query(getTasks, { entities: [Task] }), action(createTask, { entities: [Task] }), action(updateTask, { entities: [Task] }), ], })注意getTasks、createTask、updateTask都声明了entities: [Task]——这正是Entity 级缓存自动失效的触发前提任何对Task的写操作都会自动让依赖Task的查询缓存失效。而 examples/tutorials/TodoApp/src/queries.js 展示了带认证与错误处理的真实 Query 实现——通过context.user判断登录状态未登录抛HttpError(401)已登录则按当前用户过滤任务import { HttpError } from wasp/server; export const getTasks async (args, context) { if (!context.user) { throw new HttpError(401); } return context.entities.Task.findMany({ where: { user: { id: context.user.id } }, orderBy: { id: asc }, }); };这段代码同时印证了本文前面讲到的所有关键点context中既有注入的entities也有 auth 注入的user错误处理通过HttpError携带状态码。小结Wasp 的 Operations 体系把读Query 写Action两类数据操作统一为声明式 spec Node.js 实现的极简开发模式编译器自动生成前后端调用函数与 HTTP 路由context自动注入 Entity 与用户会话基于 Entity 的缓存自动失效让前端数据始终新鲜useQuery/useAction则补齐了响应式读取与乐观更新的最后一环。配合 TypeScript 生成类型从服务端实现到客户端调用形成完整的全栈类型安全闭环。你可以从 Operations 总览、Queries 详解、Actions 详解 三份文档继续深入也可以在 examples/tutorials/TodoApp 与 waspc/data/Generator/templates/sdk/wasp/client/operations/ 中查看真实应用与生成器源码。【免费下载链接】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),仅供参考
返回列表