ARTICLE DETAIL

资讯详情

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

Wasp Operations 实战指南:Queries 与 Actions 从声明、实现到缓存失效

Wasp Operations 实战指南:Queries 与 Actions 从声明、实现到缓存失效 Wasp Operations 实战指南Queries 与 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/wasp在 Wasp 中Entities 帮助你定义应用的数据模型与实体间关系而 Operations 则负责与这些数据打交道Queries 用于读取数据Actions 用于修改数据新增或更新。本文基于 Wasp 官方文档web/versioned_docs/version-0.11.8/data-model/operations/目录系统讲解 Operations 的完整使用流程——从.wasp文件中的声明、Node.js 实现、前后端调用方式到基于实体的自动缓存失效与乐观更新并辅以仓库源码佐证底层实现。读完本文你将掌握在 Wasp 应用中声明、实现并安全调用 Query 与 Action 的完整实战能力。什么是 OperationsEntities 定义了应用的数据模型与关系Operations 则负责对这些数据进行操作。Wasp 将 Operations 分为两类语义清晰、职责分明Operation用途典型场景Queries只读数据不修改服务器状态获取博客文章的全部评论、获取点赞某视频的用户列表、根据 ID 查询单个商品信息Actions写入数据新增或更新给博客文章添加评论、给视频点赞、更新商品价格两种 Operations 相互配合共同维持前端数据缓存的实时性Queries 负责读Actions 负责写Wasp 会在 Action 执行后自动使相关 Query 的缓存失效详见下文缓存失效。创建 Operation 的两步流程无论是 Query 还是 Action创建流程都高度一致只需完成两步在 Wasp 中使用query或action声明写在main.wasp/main.wasp.ts中定义该 Operation 的 Node.js 实现通常放在src/server/queries.{js,ts}或src/server/actions.{js,ts}。完成这两步后Wasp 会自动生成代码让你从客户端或服务端的任意位置以统一接口调用该 Operation。你无需自己搭建 HTTP API、管理服务端请求处理也不必处理客户端响应与缓存——只需要专注实现业务逻辑其余交给 Wasp。声明完成后Wasp 会做两件关键的事生成一个与服务端同名的 Node.js 函数生成一个与服务端同名的客户端 JavaScript 函数。该函数接收一个可选参数——一个包含任意可序列化数据的对象Wasp 会把这个对象通过网络发送出去并作为第一个位置参数传入服务端实现。这一切的底层是 Wasp 在服务端生成的 HTTP API 路由处理器route handler它会在后台调用你的 Node.js 实现。两个同名函数的生成保证了整个应用客户端与服务端拥有一致的调用接口。Queries 详解声明 Queries在main.wasp中通过query声明定义。例如声明两个 Query一个获取全部任务另一个按过滤条件如任务是否完成获取任务// ... query getAllTasks { fn: import { getAllTasks } from server/queries.js } query getFilteredTasks { fn: import { getFilteredTasks } from server/queries.js }TypeScript 注意事项即使你使用 TypeScript 并计划在src/server/queries.ts中实现导入时仍必须使用.js扩展名。Wasp 内部使用esnext模块解析要求以.js扩展名导入所有文件。这仅在导入server文件时需要。Query 的名字与其实现函数的名字不必一致但官方建议保持一致以避免混淆。可以先在.wasp文件中声明高层概念再编写具体实现。在 Node 中实现 Queries在上面的声明中我们告诉 Wasp 从src/server/queries.{js,ts}导入实现因此需要在对应文件中导出这两个函数JavaScript 版本src/server/queries.js// 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) }TypeScript 版本src/server/queries.tsimport { GetAllTasks, GetFilteredTasks } from wasp/queries/types 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 }, ] // You dont need to use the arguments if you dont need them export const getAllTasks: GetAllTasksvoid, Task[] () { return tasks } // The args object is something sent by the caller (most often from the client) export const getFilteredTasks: GetFilteredTasks PickTask, isDone, Task[] (args) { const { isDone } args return tasks.filter((task) task.isDone isDone) }Wasp 会根据.wasp文件中的声明自动生成泛型类型如GetAllTasks、GetFilteredTasks你可以用它们声明 Query 的输入与输出类型getAllTasks不接收参数输入类型为void返回任务列表输出类型为Task[]getFilteredTasks接收{ isDone: boolean }类型对象返回Task[]。为 Query 标注类型是可选的但强烈推荐——这样做可以启用全栈类型安全full-stack type safety。使用 Queries 与 useQuery hook使用 Query 时直接从wasp导入并调用即可客户端与服务端的用法完全一致import getAllTasks from wasp/queries/getAllTasks.js import getFilteredTasks from wasp/queries/getFilteredTasks.js // ... const allTasks await getAllTasks() const doneTasks await getFilteredTasks({ isDone: true })在 TypeScript 中返回值类型与 payload 会被自动推断并做类型检查import getAllTasks from wasp/queries/getAllTasks.js import getFilteredTasks from wasp/queries/getFilteredTasks.js // TypeScript automatically infers the return values and type-checks the payloads. const allTasks await getAllTasks() const doneTasks await getFilteredTasks({ isDone: true })在客户端让 Query 具备响应式reactive能力时使用useQueryhook。它随 Wasp 内置是对 react-query 的useQuery的薄封装唯一区别是无需手动提供缓存 key——Wasp 在底层自动处理import React from react import { useQuery } from wasp/queries import getAllTasks from wasp/queries/getAllTasks import getFilteredTasks from wasp/queries/getFilteredTasks 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} h2Finished Tasks/h2 {doneTasks doneTasks.length 0 ? doneTasks.map((task) Task key{task.id} {...task} /) : No finished tasks} /div ) } const Task ({ description, isDone }) { return ( div p strongDescription: /strong {description} /p p strongIs done: /strong {isDone ? Yes : No} /p /div ) } export default MainPageTypeScript 版本中你甚至不需要手动标注 Query 的返回值类型——Wasp 会根据 Query 的服务端实现自动推断客户端类型始终与服务端类型保持一致这就是全栈类型安全的体现。Queries 的错误处理出于安全考虑Query 的 Node.js 实现中抛出的所有异常都会以 HTTP 状态码500返回给客户端且移除所有细节。默认隐藏错误详情可以避免敏感信息通过网络泄露。如果确实需要向客户端传递额外错误信息可以在实现中构造并抛出HttpErrorimport HttpError from wasp/core/HttpError.js export const getAllTasks async (args, context) { throw new HttpError( 403, // status code You cant do this!, // message { foo: bar } // data ) }当状态码为4xx时客户端会收到包含对应message和data字段的响应对象并重新抛出该错误包含这些字段对于其他 HTTP 状态码服务端不会转发这些字段以防止信息泄露。在 Queries 中使用 EntitiesQuery 中使用的资源绝大多数情况下是 Entities。要使用某个 Entity在query声明中加入entities字段query getAllTasks { fn: import { getAllTasks } from server/queries.js, entities: [Task] } query getFilteredTasks { fn: import { getFilteredTasks } from server/queries.js, entities: [Task] }Wasp 会把指定的 Entity 注入 Query 的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 中的prisma.task能力如findMany、create、update等。Queries API 参考query声明支持的字段fn: ServerImport必填Query 的 Node.js 实现的导入语句entities: [Entity]希望在 Query 内使用的 Entity 列表使用方式见上文在 Queries 中使用 Entities。Query 实现的函数签名Query 的实现是一个接收两个参数的 Node.js 函数需要await时可声明为async。两个参数均为位置参数名称可自定义官方惯例为args和contextargs类型取决于 Query调用 Query 时传入的数据对象如过滤条件context类型取决于 QueryWasp 传入的上下文对象包含用户会话信息与 Entity 信息。Entity 用法见上文user对象的用法见 auth 章节。在 TypeScript 中声明 Query 后 Wasp 会生成泛型类型。对于声明为getSomething的 Query生成的类型名为GetSomethingimport { GetSomething } from wasp/queries/types它接受两个可选的类型参数Inputargs对象的类型Query 输入 payload默认值为neverOutputQuery 返回值的类型Query 输出 payload默认值为unknown。默认值的设计使类型签名尽可能宽松如果希望 Query 不接收/不返回任何内容请使用void作为类型参数。useQueryhook 的三个参数queryFn必填Wasp 根据.wasp文件中的query声明生成的客户端 Query 函数queryFnArgs希望传入 Query 的参数对象payloadQuery 的 Node.js 实现会将其作为第一个位置参数接收optionsreact-query 的 options 对象用于修改该 Query 的默认行为如需修改全局默认值可以在客户端配置函数中设置。Actions 详解Actions 与 Queries 在 API 层面几乎完全一致关键区别在于Actions 被设计用于修改和新增数据而 Queries 仅用于读取数据。Actions 与 Queries 协同工作保持数据缓存的新鲜度。声明 Actions在main.wasp中通过action声明定义。例如声明两个 Action一个创建任务一个将任务标记为完成// ... action createTask { fn: import { createTask } from server/actions.js } action markTaskAsDone { fn: import { markTaskAsDone } from server/actions.js }TypeScript 用户的注意事项与 Query 相同实现文件即使命名为src/server/actions.ts在 Wasp 声明中导入时也必须使用.js扩展名esnext模块解析所致。Action 的名字与其实现函数名不必一致官方建议保持一致。声明 Action 后同样会生成服务端与客户端两个同名函数保证统一调用接口。在 Node 中实现 Actions根据声明在src/server/actions.{js,ts}中导出实现JavaScript 版本src/server/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 }, ] // You dont need to use the arguments if you dont need them export const createTask (args) { const newTask { id: nextId, isDone: false, description: args.description, } nextId 1 tasks.push(newTask) return newTask } // The args object is something sent by the caller (most often from the client) export const markTaskAsDone (args) { const task tasks.find((task) task.id args.id) if (!task) { // Well show how to properly handle such errors later return } task.isDone true }TypeScript 版本src/server/actions.tsimport { CreateTask, MarkTaskAsDone } from wasp/actions/types 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 }, ] // You dont need to use the arguments if you dont need them export const createTask: CreateTaskPickTask, description, Task ( args ) { const newTask { id: nextId, isDone: false, description: args.description, } nextId 1 tasks.push(newTask) return newTask } // The args object is something sent by the caller (most often from the client) export const markTaskAsDone: MarkTaskAsDonePickTask, id, void ( args ) { const task tasks.find((task) task.id args.id) if (!task) { // Well show how to properly handle such errors later return } task.isDone true }Wasp 会根据声明自动生成泛型类型CreateTask、MarkTaskAsDonecreateTask接收{ description: string }类型对象返回新建的任务类型TaskmarkTaskAsDone接收{ id: number }类型对象不返回任何内容返回类型为void。标注类型同样可选但强烈推荐它带来全栈类型安全。使用 Actions 与 useAction hook使用 Action 时直接从wasp导入并调用客户端与服务端用法一致import createTask from wasp/actions/createTask.js import markTasAsDone from wasp/actions/markTasAsDone.js // ... const newTask await createTask({ description: Learn TypeScript }) await markTasAsDone({ id: 1 })TypeScript 会自动推断返回值并检查 payloadimport createTask from wasp/actions/createTask.js import markTasAsDone from wasp/actions/markTasAsDone.js // TypeScript automatically infers the return values and type-checks the payloads. const newTask await createTask({ description: Keep learning TypeScript }) await markTasAsDone({ id: 1 })在客户端组件中使用 Action 时由于 Action 不需要响应式能力可以直接调用import React from react import { useQuery } from wasp/queries import getTask from wasp/queries/getTask import markTaskAsDone from wasp/actions/markTaskAsDone 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 ) }Wasp 还提供useActionhook 用于增强 Action例如添加乐观更新详见下文useAction hook 与乐观更新。Actions 的错误处理与 Query 一致Action 实现中抛出的异常默认以500返回客户端且移除细节如需传递额外信息构造并抛出HttpErrorimport HttpError from wasp/core/HttpError.js export const createTask async (args, context) { throw new HttpError( 403, // status code You cant do this!, // message { foo: bar } // data ) }4xx状态码会向客户端透传message和data字段其他状态码不会。在 Actions 中使用 Entities在action声明中加入entities字段action createTask { fn: import { createTask } from server/actions.js, entities: [Task] } action markTaskAsDone { fn: import { markTaskAsDone } from server/actions.js, entities: [Task] }Wasp 将指定 Entity 注入 Action 的context参数同时会通过查看每个 Action/Query 使用的 Entities 来失效前端 Query 缓存详见下文缓存失效// The args object is the payload sent by the caller (most often from the client) 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 }, }) }context.entities.Task暴露的是 Prisma CRUD API 中的prisma.task能力。Prisma 错误助手在 Operations 中你可能希望把常见的 Prisma 错误转换为 HTTP 友好响应。Wasp 为此暴露了两个辅助函数isPrismaError和prismaErrorToHttpError。目前 Wasp 将两种特定的 Prisma 错误转换为对应 HTTP 错误后续会继续扩充其余错误保持为500。import { isPrismaError, prismaErrorToHttpError } from wasp/utils.js; // ... try { await context.entities.Task.create({...}) } catch (e) { if (isPrismaError(e)) { throw prismaErrorToHttpError(e) } else { throw e } }Actions API 参考action声明支持的字段fn: ServerImport必填Action 的 Node.js 实现的导入语句entities: [Entity]希望在 Action 内使用的 Entity 列表。Action 实现的函数签名与 Query 相同——一个接收args与context两个位置参数的 Node.js 函数可声明为asyncargs类型取决于 Action调用 Action 时传入的数据对象context类型取决于 ActionWasp 传入的上下文对象包含用户会话信息与 Entity 信息。user对象的用法见 auth 章节。在 TypeScript 中声明 Action 后 Wasp 会生成泛型类型。对于声明为createSomething的 Action生成的类型名为CreateSomethingimport { CreateSomething } from wasp/actions/types同样接受两个可选类型参数Inputargs类型默认never与Output返回值类型默认unknown若不需要传入/返回任何内容使用void。useAction hook 与乐观更新阅读本节前建议先理解 Queries 与缓存失效的工作方式。在组件中使用 Actions 时可以用useActionhook 增强它们。该 hook 随 Wasp 内置用于装饰 Wasp Action它返回一个与原始 Action API 匹配的函数同时在底层做额外的事情取决于配置。useAction接受两个参数actionFn必填希望增强的 Wasp Action即 Wasp 根据 Action 声明生成的客户端 Action 函数actionOptions配置想要附加到该 Action 上的额外功能的选项对象。虽然该参数技术上可选但不提供它就没有使用useAction的意义与直接调用 Action 相同。选项对象支持以下字段optimisticUpdates一个对象数组每个对象定义一个要对 Query 缓存执行的乐观更新。定义乐观更新需要指定两个属性getQuerySpecifier必填一个返回 Query 说明符specifier即用于定位要更新的 Query 的值的函数。Query 说明符是一个数组指定查询函数与参数。例如要为useQuery(fetchFilteredTasks, { isDone: true })所使用的 Query 做乐观更新getQuerySpecifier需返回数组[fetchFilteredTasks, { isDone: true }]。Wasp 会把传入被装饰 Action 的参数转发给该函数即你可以利用新增/变更条目的属性来定位 QueryupdateQuery必填执行乐观更新的函数返回缓存的目标状态。Wasp 会以两个参数调用它item传入被装饰 Action 的参数和oldData说明符所标识 Query 的当前缓存值。注意updateQuery必须是纯函数——它必须返回getQuerySpecifier所标识的缓存目标值且不得产生任何副作用。同时请确保只更新由该 Action 引起的、受乐观更新影响的 Query 缓存Wasp 目前无法校验这一点。最后updateQuery的实现应能正确处理oldData的任何状态例如不要依赖数组位置。下面是一个完整示例为 ActionmarkTaskAsDone将任务的isDone状态切换为完成配置乐观更新import React from react import { useQuery } from wasp/queries import { useAction } from wasp/actions import getTask from wasp/queries/getTask import markTaskAsDone from wasp/actions/markTaskAsDone 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类型从wasp/actions导入以获得类型检查import { useAction, OptimisticUpdateDefinition } from wasp/actions; // ... const markTaskAsDoneOptimistically useAction(markTaskAsDone, { optimisticUpdates: [ { getQuerySpecifier: ({ id }) [getTask, { id }], updateQuery: (_payload, oldData) ({ ...oldData, isDone: true }), } as OptimisticUpdateDefinitionTaskPayload, Task, ], });高级用法useActionhook 目前只支持指定乐观更新未来版本会提供更多功能。Wasp 的乐观更新 API 刻意保持精简只专注于更新 Query 缓存这是最常见的用例。如果你需要更灵活、控制力更强的 API可以改用 react-query 的useMutationhook 直接操作其底层 API。此时你需要访问 Query 缓存 key——Wasp 内部使用该 key 但对程序员做了抽象你可以通过任何 Query 上的queryCacheKey属性轻松获取import getTasks from wasp/queries/getTasks const queryKey getTasks.queryCacheKey缓存失效自动基于实体的 Query 缓存更新管理 Web 应用状态最棘手的部分之一是确保 Query 返回的数据保持最新。由于 Wasp 使用 react-query 管理 Query必须保证 Query更准确地说是它们由 react-query 管理的缓存结果在过期时被失效。虽然可以借助 react-query 提供的多种机制手动失效缓存如 refetch、直接失效但手动失效很快就会变得复杂且容易出错。因此 Wasp 提供了一种更快、更有效的开箱即用方案基于实体的自动 Query 缓存失效。由于 Action 可以而且通常确实修改状态而 Query 读取状态Wasp 会在某个使用了同一实体的 Action 被执行时失效该实体的 Query 缓存。例如如果 ActioncreateTask和 QuerygetTasks都使用 EntityTask那么执行createTask就可能导致getTasks的缓存结果过期。作为响应Wasp 会失效该缓存使getTasks从服务器重新获取数据并更新。在实践中这意味着 Wasp 无需你考虑缓存失效即可保持 Query新鲜。另一方面这种自动缓存失效可能会有些浪费某些更新可能不必要并且只对 Entities 生效。如果这是个问题你可以暂时使用 react-query 提供的机制并期待 Wasp 在未来以更优雅的方式支持这些用例。如果希望在执行 Action 后乐观地设置缓存值可以使用乐观更新上文useAction部分这是目前 Wasp 原生支持的唯一手动缓存失效机制其他情况可依赖 react-query。Queries 与 Actions 的关键差异Queries 与 Actions 是 Wasp 中两个紧密相关的概念看起来可能在做类似的事情但 Wasp 对它们区别对待。关键差异如下读写职责Action 可以而且经常应该修改服务器状态而 Query 只允许读取。Wasp 在执行缓存失效时依赖你遵守这一约定因此严格遵守它至关重要响应式需求Action 不需要响应式可以直接调用不过 Wasp 也提供useActionReact hook 为 Action 添加额外行为如乐观更新声明结构action声明与query声明基本一致唯一的区别在于声明名称本身。从源码看 Operations 的实现Wasp 的编译器核心用 Haskell 编写。从源码看query与action声明在编译器的应用规范AppSpec层面被建模为结构几乎相同的数据类型Query.hs 中Query数据类型包含三个字段fn :: ExtImport服务端实现的外部导入、entities :: Maybe [Ref Entity]可选的实体引用列表以及auth :: Maybe Bool是否要求认证Action.hs 中Action数据类型拥有完全一致的字段结构。这印证了文档中的说明query声明与action声明几乎完全相同唯一区别在声明名称。两者都通过fn关联到src/server/下的 Node.js 实现通过entities声明依赖的实体用于上下文注入与缓存失效并可选地开启auth认证约束。此外两个模块都实现了Inspectable支持将导入信息、实体列表与认证开关以可检查inspectable的形式暴露出来——这正是waspCLI 中wasp info等内省命令展示 Operation 信息的来源。序列化细节superjsonWasp 在底层使用 superjson 作为序列化方案。这意味着你不必只发送和接收 JSON payload——任何 superjson 兼容的 payload如Date、Set、List、循环引用等都可以发送和接收由 Wasp 负责反序列化。在 TypeScript 中只要用正确自动生成的类型标注 Query/ActionTypeScript 就能保证你的 payload 是合法的即 Wasp 知道如何序列化和反序列化它们。总结Wasp 的 Operations 体系把读与写两条数据通道清晰分离Queries 只读、Actions 只写二者共用统一的声明.wasp→ 实现src/server/→ 调用wasp/...三步式开发体验。你既可以在服务端直连 Prisma API也可以在客户端通过useQuery/useAction获得响应式与乐观更新能力而基于实体的自动缓存失效让数据一致性问题的处理成本大幅降低。掌握本文的声明语法、实现签名、错误处理约定与缓存失效机制即可在 Wasp 应用中写出类型安全、行为可预期的前后端数据层代码。【免费下载链接】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),仅供参考
返回列表