
深入 Wasp Query在 TodoApp 中实现数据库查询与前端响应式渲染【免费下载链接】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/waspWasp 中的 Query 是读写数据Operations体系里的读半边它让开发者以声明方式定义数据读取接口由框架自动生成客户端调用函数、HTTP 路由与缓存失效逻辑。本文以 TodoApp 教程web/versioned_docs/version-0.14/tutorial/05-queries.md为骨架完整讲解在main.wasp中声明 Query、在 NodeJS 中实现 Query、在前端用useQuery消费 Query 的全过程并结合仓库中的真实实现与生成器源码说明其底层原理。读完本文你将掌握 Wasp 中数据读取的标准写法、args/context参数的语义、全栈类型安全的机制以及如何正确处理查询错误。Operations 概览Query 与 Action 的分工在 Wasp 中与 Entity实体打交道的核心方式是Queries 与 Actions二者统称为Operations操作。相关说明可参考 web/versioned_docs/version-0.14/data-model/operations/overview.mdQuery 用于读取数据例如获取一篇博客文章下的所有评论、某个视频的点赞用户列表、根据 ID 查询某个商品的信息Action 用于修改数据创建、更新、删除实体。TodoApp 中我们希望列出所有任务这是纯读取操作因此使用 Query。完成这个目标需要两步创建一个从数据库取回任务的 Query修改MainPage.{jsx,tsx}用该 Query 获取结果并展示在页面上。第一步在 main.wasp 中声明 Query要让 Wasp知道一个 Query 的存在必须在 Wasp 声明文件中添加query声明。在 0.14 版本中声明文件是main.wasp当前仓库的主干示例已演进为 examples/tutorials/TodoApp/main.wasp.ts 的 TS spec 写法但 0.14 教程对应的.wasp语法如下。query 声明语法解析以getTasks为例JavaScript 与 TypeScript 项目的声明完全一致// ... query getTasks { // Specifies where the implementation for the query function is. // The path src/queries resolves to src/queries.js (或 src/queries.ts)。 // No need to specify an extension. fn: import { getTasks } from src/queries, // Tell Wasp that this query reads from the Task entity. Wasp will // automatically update the results of this query when tasks are modified. entities: [Task] }声明中两个字段的含义fn必填Query 实现的导入声明。import { getTasks } from src/queries告诉 Wasp 去src/queries.js或src/queries.ts中找到名为getTasks的具名导出无需写文件扩展名。entities可选该 Query 需要使用的实体列表。声明了[Task]之后Wasp 会把Task实体的 Prisma Client 注入到 Query 的context中并在任务数据被修改时自动刷新相关查询结果。Wasp 中 Query 的名字与实现函数的名字并不要求一致只是教程中保持同名以免混淆。从源码结构看query声明的完整字段规范可在 web/versioned_docs/version-0.14/data-model/operations/queries.md 的 API Reference 中查到fn为必填的 ExtImportentities为实体列表。声明之后 Wasp 做了什么完成query声明后Wasp 会做两件重要的事情在服务端生成一个与 Query 同名的NodeJS 函数用于真正执行业务逻辑在客户端生成一个与 Query 同名的JavaScript 函数例如getTasks它接收一个可选参数——包含任意可序列化数据的对象。Wasp 会把这个对象通过网络发送出去并作为第一个位置参数传给 Query 的实现。这套抽象之所以成立是因为 Wasp 在服务端自动生成了一条 HTTP API 路由处理器在底层调用 Query 的 NodeJS 实现。两端同名函数的生成保证了整个应用客户端与服务端拥有一致的调用接口。对应的生成器源码位于 waspc/src/Wasp/Generator/SdkGenerator/Client/OperationsGenerator.hs 与 waspc/src/Wasp/Generator/SdkGenerator/Server/OperationsGenerator.hs而服务端路由的生成逻辑在 waspc/src/Wasp/Generator/ServerGenerator/OperationsRoutesG.hs。第二步实现 Query 的 NodeJS 函数声明完成之后创建新文件src/queries.js或src/queries.ts导出刚才在声明中 import 的函数。JavaScript 实现export const getTasks async (args, context) { return context.entities.Task.findMany({ orderBy: { id: asc }, }) }TypeScript 实现与自动生成的类型import { Task } from wasp/entities import { type GetTasks } from wasp/server/operations export const getTasks: GetTasksvoid, Task[] async (args, context) { return context.entities.Task.findMany({ orderBy: { id: asc }, }) }TS 版本中Task与GetTasks两个类型均由 Wasp 根据main.wasp的内容自动生成Task对应你在schema.prisma中定义的Task实体实体定义过程见 web/versioned_docs/version-0.14/tutorial/04-entities.mdGetTasksWasp 根据getTasksQuery 声明自动生成的泛型类型。生成类型是泛型接受两个可选的类型参数Input——args对象Query 的输入载荷的类型默认值为neverOutput——Query 返回值输出载荷的类型默认值为unknown。默认值被设计得尽量宽松如果 Query 不需要输入/输出用void显式标注即可。上面的getTasks不接收任何参数输入类型为void但返回任务数组输出类型为Task[]。给 Query 标注类型是可选的但强烈推荐——这会带来full-stack type safety全栈类型安全。如果不想显式标注返回值类型可以用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 GetFoo这样 TypeScript 既能推断出context的正确类型也能推断出返回类型为{ foos: Foo[], message: string, queriedAt: Date }。args 与 contextQuery 函数的两个关键参数Query 实现是一个普通的 NodeJS 函数需要await时写成async按位置接收两个参数参数名可自取约定为args与contextargs: object——调用方传入的参数对象例如过滤条件。调用时如何传入见后文使用 Queries部分的示例context——由 Wasp 注入的附加上下文对象其类型取决于 Query 声明。它包含用户会话信息如果 Query 使用了 auth可通过context.user访问当前用户以及实体相关信息。由于main.wasp中声明getTasks使用了Task实体Wasp 便把Task实体的 Prisma Client 以context.entities.Task的形式注入上面正是用它执行了findMany来获取全部任务。context.entities.Task暴露的是 Prisma 的 CRUD API。请注意Queries 和 Actions 都是在服务端执行的 NodeJS 函数。虽然你可以在客户端代码里 import 它们但真正执行的位置是服务端。第三步在前端调用 Query虽然 Query 实现在服务端但 Wasp 会生成客户端函数自动处理序列化、网络请求和缓存失效让你可以像调用普通函数一样调用服务端代码。因此在 React 组件里使用刚创建的getTasks非常直接JavaScript 版 MainPage.jsximport { getTasks, useQuery } from wasp/client/operations export const MainPage () { const { data: tasks, isLoading, error } useQuery(getTasks) return ( div {tasks TasksList tasks{tasks} /} {isLoading Loading...} {error Error: error} /div ) } const TaskView ({ task }) { return ( div input typecheckbox id{String(task.id)} checked{task.isDone} / {task.description} /div ) } const TasksList ({ tasks }) { if (!tasks?.length) return divNo tasks/div return ( div {tasks.map((task, idx) ( TaskView task{task} key{idx} / ))} /div ) }TypeScript 版 MainPage.tsximport { Task } from wasp/entities import { getTasks, useQuery } from wasp/client/operations export const MainPage () { const { data: tasks, isLoading, error } useQuery(getTasks) return ( div {tasks TasksList tasks{tasks} /} {isLoading Loading...} {error Error: error} /div ) } const TaskView ({ task }: { task: Task }) { return ( div input typecheckbox id{String(task.id)} checked{task.isDone} / {task.description} /div ) } const TasksList ({ tasks }: { tasks: Task[] }) { if (!tasks?.length) return divNo tasks/div return ( div {tasks.map((task, idx) ( TaskView task{task} key{idx} / ))} /div ) }这段代码绝大部分是普通 React唯一的例外是那几个特殊的waspimportgetTasks——Wasp 根据main.wasp中的getTasks声明生成的客户端 Query 函数useQuery——Wasp 的响应式 React 钩子基于 react-query 的同名钩子封装而来Task仅 TS——schema.prisma中Task实体的类型。注意 TS 版本中你不需要标注 Query 返回值的类型Wasp 会把你实现 Query 时定义的类型直接用于生成的客户端函数。这就是全栈类型安全——客户端上的类型与服务端永远保持一致。useQuery 钩子直接调用与响应式调用的区别你也可以直接调用getTasks()获取数据但useQuery钩子让调用变得响应式每次 Query 结果变化时React 都会重新渲染组件。而 Wasp 会在数据被修改时自动刷新 Query——这正是声明entities: [Task]带来的效果。useQuery钩子接收三个参数完整规范见 web/versioned_docs/version-0.14/data-model/operations/queries.md 的 API ReferencequeryFn必填——Wasp 根据.wasp文件中的query声明生成的客户端查询函数queryFnArgs——希望传给 Query 的参数对象载荷Query 的 NodeJS 实现会把它作为第一个位置参数接收options——react-query 的options对象用于调整该 Query 的默认行为如需修改全局默认行为可以在客户端 setup 函数中配置。与原生 react-query 相比Wasp 的useQuery唯一的不同是不需要你提供缓存 key——Wasp 在底层自动处理。错误处理HttpError出于安全考虑Query 的 NodeJS 实现中抛出的所有异常默认都会以 HTTP 状态码500返回给客户端并剥离全部细节。隐藏错误细节可以防止敏感信息经网络意外泄露。如果确实希望向客户端传递额外的错误信息可以在实现中构造并抛出HttpErrorimport { type GetAllTasks } from wasp/server/operations import { HttpError } from wasp/server export const getAllTasks: GetAllTasks async (args, context) { throw new HttpError( 403, // status code You cant do this!, // message { foo: bar } // data ) }当状态码为4xx时客户端会收到包含对应message与data字段的响应对象并重新抛出包含这些字段的错误对于其他 HTTP 状态码服务端不会转发这些字段以防止信息泄露。在服务端调用 Query在服务端调用 Query 与在客户端类似只有两点不同从wasp/server/operations而不是wasp/client/operations导入对于需要认证的 Query需要显式传入包含 user 的 context 对象例如从某个 Operation 的context.user中取得AuthUser。import { getAllTasks, getFilteredTasks } from wasp/server/operations const user // 获取一个 AuthUser 对象例如从某个 operation 的 context.user 中取得 // TypeScript 会自动推断返回值并校验载荷类型 const allTasks await getAllTasks({ user }) const doneTasks await getFilteredTasks({ isDone: true }, { user })使用带参数的 QuerygetFilteredTasks 示例前面教程中的getTasks不带参数但实际业务中按条件过滤是常态。参考 web/versioned_docs/version-0.14/data-model/operations/queries.md 中的getFilteredTasks示例可以同时看到带参数的 Query与结合 Entity 使用的完整写法query getAllTasks { fn: import { getAllTasks } from src/queries.js, entities: [Task] } query getFilteredTasks { fn: import { getFilteredTasks } from src/queries.js, entities: [Task] }import { type Task } from wasp/entities import { type GetAllTasks, type GetFilteredTasks } from wasp/server/operations export const getAllTasks: GetAllTasksvoid, Task[] async (args, context) { return context.entities.Task.findMany({}) } export const getFilteredTasks: GetFilteredTasks PickTask, isDone, Task[] async (args, context) { return context.entities.Task.findMany({ where: { isDone: args.isDone }, }) }在客户端调用import { getAllTasks, getFilteredTasks } from wasp/client/operations const allTasks await getAllTasks() const doneTasks await getFilteredTasks({ isDone: true })在 React 中组合两个 Queryimport React from react import { type Task } from wasp/entities import { useQuery, getAllTasks, getFilteredTasks } from wasp/client/operations const MainPage () { // TypeScript 自动推断返回值并校验载荷类型 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 }: Task) { return ( div p strongDescription: /strong {description} /p p strongIs done: /strong {isDone ? Yes : No} /p /div ) } export default MainPage注意useQuery(getFilteredTasks, { isDone: true })第二个参数就是queryFnArgs它会被完整地传给服务端实现作为args使用。TypeScript 会根据服务端GetFilteredTasksPickTask, isDone, Task[]的类型自动校验这里的载荷形状。运行验证与真实仓库中的完整实现完成上述修改后运行wasp start页面上应该显示 No tasks 文本——因为数据库里还没有任何数据创建任务表单是教程的下一步内容对应 web/versioned_docs/version-0.14/tutorial/06-actions.mdAction 的用法。仓库中的 examples/tutorials/TodoApp/src/queries.js 给出了一个比教程更完整的带认证校验的真实实现可以对照学习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.user的用法当应用启用了 auth 时Query 的context中会注入当前登录用户这里用它判断用户是否登录HttpError(401)的用法未登录时直接抛出 401 错误对应上文错误处理一节按用户过滤数据where: { user: { id: context.user.id } }确保每个用户只能看到自己的任务——这正是把实体注入context.entities后 Prisma 完整查询能力的体现。对应的声明当前主干版本的 TS spec 写法位于 examples/tutorials/TodoApp/main.wasp.tsimport { getTasks } from ./src/queries with { type: ref }; export default app({ // ... spec: [ // ... query(getTasks, { entities: [Task] }), ], });可见声明 实体注入 自动生成的模型在不同版本中保持一致query(getTasks, { entities: [Task] })与 0.14 教程的query getTasks { fn: import { getTasks } from src/queries, entities: [Task] }表达的是同一件事——声明查询、绑定实体、交给 Wasp 生成两端的调用代码。小结Query 的关键要点Query 负责读取数据与负责修改数据的 Action 共同构成 Wasp 的 Operations 体系总览见 web/versioned_docs/version-0.14/data-model/operations/overview.md创建 Query 只需两步在main.wasp中声明query声明 fnentities在src/queries.{js,ts}中实现(args, context) ...声明entities后实体对应的 Prisma Client 会注入context.entities并且 Wasp 会在数据变化时自动刷新相关查询客户端从wasp/client/operations导入同名函数配合useQuery钩子获得响应式数据流无需手动管理缓存 key 与网络请求TypeScript 项目中 Wasp 自动生成GetTasks这类泛型类型实现服务端定义类型、客户端自动继承的全栈类型安全错误处理遵循默认隐藏细节、HttpError显式透传的约定详细 API 见 web/versioned_docs/version-0.14/data-model/operations/queries.md。【免费下载链接】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),仅供参考