news 2026/9/13 20:24:44

Wasp Queries 实战指南:用声明式方式实现只读数据查询与全栈类型安全

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Wasp Queries 实战指南:用声明式方式实现只读数据查询与全栈类型安全

Wasp Queries 实战指南:用声明式方式实现只读数据查询与全栈类型安全

【免费下载链接】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(查询)机制让你无需手写 HTTP API、服务端路由与客户端缓存逻辑,即可从服务端安全地读取数据并在全栈范围内调用。本文基于仓库内 web/docs/data-model/operations/queries.md 官方文档展开,结合 examples/kitchen-sink 的真实示例与 @wasp.sh/spec 源码,完整讲解 Query 的声明、实现、调用、错误处理与类型安全,读完即可在 Wasp 应用中落地可复用的只读数据层。

Query 是什么:Wasp 中的只读数据操作

在 Wasp 的数据模型体系中,Entities 负责定义应用的数据结构与关系,而Operations(操作)负责与这些数据打交道。Operations 分为两类:

  • Queries(查询):只读数据,不修改服务端状态;
  • Actions(操作):修改或新增数据。

在 operations 总览文档 中,二者的分工被概括为一句话:Queries 用于读取数据,Actions 用于改变数据(更新既有记录或创建新记录)。

适合使用 Query 的场景非常典型:拉取一篇博客文章下的所有评论、获取点赞某个视频的用户列表、根据 ID 查询某个产品的详情——这些只读操作都是 Query 的完美用例。

:::tip 与 Action 的对比 Query 与 Action 在 API 上高度相似,Action 的指南同样适用于理解 Query。两者的核心差异在于:Query 只允许读取服务端状态,Action 可以(且通常应该)修改服务端状态。Wasp 依赖这一约定来进行前端缓存失效,因此遵守"读用 Query、写用 Action"的规范至关重要。详细差异见下文"Query 与 Action 的异同"一节。 :::

创建 Query 的两步流程

创建一个 Query 只需两步:

  1. 在 Wasp 文件中使用queryspec 声明 Query
  2. 实现 Query 的 NodeJS 函数

完成这两步后,Wasp 会自动生成对应的客户端与服务端调用代码,你可以在代码库的任意位置(客户端或服务端)使用该 Query。你无需自己构建 HTTP API、管理服务端请求处理,甚至无需关心客户端的响应处理和缓存——只需要专注于 Query 内部的业务逻辑,剩下的交给 Wasp。

声明 Query(Specifying Queries)

main.wasp.ts中,通过query构造函数声明 Query。以下示例声明了两个 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), ], })

注意导入语句末尾的with { type: "ref" }:根据 参考导入说明,这告诉 Wasp 把导入当作对应用代码的引用,而不会真正执行被导入的代码。更完整的参考导入语法说明见 web/docs/general/spec.md 中的 "reference imports" 一节。

此处你引用的是尚不存在的实现函数——这没关系。Wasp 的理念是先有高层概念(Wasp 文件中的 Query spec),再处理实现细节(JavaScript 中的 Query 实现)

声明之后,Wasp 会从传给query的函数名推导出 Query 的名称query(getFilteredTasks)会创建一个名为getFilteredTasks的 Query。随后发生两件重要的事:

  • Wasp生成一个以 Query 命名的服务端 NodeJS 函数
  • Wasp生成一个以 Query 命名的客户端 JavaScript 函数(如getFilteredTasks)。该函数接收一个可选的参数——一个包含任意可序列化数据的对象,Wasp 会通过网络发送该对象,并将其作为第一个位置参数传入 Query 的实现。

这种抽象之所以成立,是因为 Wasp 在服务端生成了一个 HTTP API 路由处理器,在其内部调用 Query 的 NodeJS 实现。生成的这两个同名函数,保证了整个应用(客户端与服务端)拥有一致的调用接口。

实现 Query(Implementing Queries in Node)

声明之后,需要在src/queries.{js,ts}中导出实现(Wasp 会从这里查找)。下面是getAllTasksgetFilteredTasks的完整实现:

JavaScript 版本(src/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 don't need to use the arguments if you don't 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/queries.ts):

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 }, ] // You don't need to use the arguments if you don't need them export const getAllTasks: GetAllTasks<void, Task[]> = () => { return tasks } // The 'args' object is something sent by the caller (most often from the client) export const getFilteredTasks: GetFilteredTasks< Pick<Task, "isDone">, Task[] > = (args) => { const { isDone } = args return tasks.filter((task) => task.isDone === isDone) }

Payload 序列化约束(superjson):根据 操作文档附注,Wasp 底层使用 superjson 进行序列化。这意味着你不只限于发送和接收 JSON 载荷——bigintDateMapSet以及Prisma.Decimal等 superjson 支持的额外类型都会由 Wasp 自动处理序列化与反序列化。在 TypeScript 中,只要用正确的自动生成类型标注 Operation,编译器就能保证 payload 是合法的(即 Wasp 知道如何序列化/反序列化它们)。

Query 的类型支持

Wasp 会根据 Wasp 文件中的 spec 自动生成GetAllTasksGetFilteredTasks这类泛型类型,用来标注 Query 实现。这是可选的,但非常有用,因为正确标注后:

  • TypeScript 会知道context.entities对象必须包含Task实体;
  • TypeScript 会知道context对象是否包含用户信息(取决于 Query 是否使用 auth)。

生成的类型接受两个可选类型参数

  1. Input——Query 函数接收的参数(payload)类型;
  2. Output——Query 函数的返回类型。

用上面的代码举例:getAllTasks不接收任何参数(输入类型为void),但返回任务列表(输出类型为Task[]);getFilteredTasks期望接收{ isDone: boolean }类型的对象(由Task实体类型派生)。如果省略两个类型参数,TypeScript 会推断最宽泛的类型(输入为never、输出为unknown)。如果你不希望 Query 接收或返回任何值,请使用void作为类型参数。

指定Input/Output完全是可选的,但强烈推荐,它能带来:

  • 在实现内部获得参数与返回值的类型支持;
  • 全栈类型安全(full-stack type safety)——客户端调用处的类型永远与服务端实现匹配。
satisfies推断返回类型

如果不想显式写出 Query 的返回类型,可以用 TypeScript 的satisfies关键字让编译器自动推断:

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的正确类型,以及 Query 的返回类型是{ foos: Foo[], message: string, queriedAt: Date }。如果不需要context,甚至可以完全跳过类型标注与参数:

const getFoo = () => ({ name: "Foo", date: new Date() })

使用 Query

在客户端调用 Query

在客户端,从wasp/client/operations导入 Query 并直接调用即可:

import { getAllTasks, getFilteredTasks } from "wasp/client/operations" // ... const allTasks = await getAllTasks() const doneTasks = await getFilteredTasks({ isDone: true })

调用方式不因 Query 是否要求登录而改变——Wasp 会在后台自动认证已登录用户。

在 TypeScript 中,客户端代码会自动获得类型安全:

import { getAllTasks, getFilteredTasks } from "wasp/client/operations" // TypeScript automatically infers the return values and type-checks // the payloads. const allTasks = await getAllTasks() const doneTasks = await getFilteredTasks({ isDone: true })

你只需要在服务端实现中指定 Query 的类型,客户端代码就会自动知道其 API payload 类型——这就是 Wasp 的自动全栈类型安全

在服务端调用 Query

在服务端调用 Query 与客户端类似,但有两点不同:

  • wasp/server/operations而不是wasp/client/operations导入;
  • 对于需要认证的 Query,必须传入带有user字段的context对象——context的其他部分(如 Entities)无需手动传入,会自动注入。
import { getAllTasks, getFilteredTasks } from "wasp/server/operations" const user = // Get an AuthUser object, e.g., from context.user in an operation. // ... const allTasks = await getAllTasks({ user }) const doneTasks = await getFilteredTasks({ isDone: true }, { user })

TypeScript 版本同样会自动推断返回值并做 payload 类型检查。

useQuery钩子实现响应式查询

在客户端使用 Query 时,可以用useQuery钩子让数据响应式。这个钩子随 Wasp 内置,是react-queryuseQuery钩子的薄封装,唯一区别是你无需提供缓存 key——Wasp 会在底层自动处理

下面是完整的组件示例(src/MainPage.jsx/src/MainPage.tsx):

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 <div>There was an error</div> } return ( <div> <h2>All Tasks</h2> {allTasks && allTasks.length > 0 ? allTasks.map((task) => <Task key={task.id} {...task} />) : "No tasks"} <h2>Finished 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> <strong>Description: </strong> {description} </p> <p> <strong>Is done: </strong> {isDone ? "Yes" : "No"} </p> </div> ) } export default MainPage

TypeScript 版本中,你同样不需要手动标注 Query 的返回值类型——Wasp 会自动从后端实现推断。这正是"全栈类型安全"的含义:客户端上的类型永远与服务端一致。注意useQuery的第一个参数直接传入 Query 函数本身(如getAllTasks),第二个参数是传给 Query 的 payload 对象(如{ isDone: true })。

错误处理

出于安全考虑,Query 的 NodeJS 实现中抛出的所有异常,都会以 HTTP 状态码500发送给客户端,并且移除所有其他细节。默认隐藏错误细节,有助于避免通过网络意外泄露敏感信息。

如果确实想向客户端传递额外的错误信息,可以在实现中构造并抛出HttpError

import { 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 can't do this!", // message { foo: "bar" } // data ) }

当状态码为4xx时,客户端会收到包含对应messagedata字段的响应对象,并重新抛出包含这些字段的错误。为防止信息泄露,对于其他任何 HTTP 状态码,服务端都不会转发这些字段。

这一点在仓库示例中得到了实践:在 examples/kitchen-sink/src/features/operations/queries.ts 中,getTask在任务不存在时抛出HttpError(404),而当任务属于其他用户时也抛出HttpError(404)——代码注释明确说明,这是为了"隐藏被禁止访问的目标资源是否存在"(防范 IDOR 类漏洞的安全措施)。

在 Query 中使用 Entities

大多数情况下,Query 操作的资源是 Entities。要在 Query 中使用 Entity,需要在queryspec 中把它加入entities选项:

import { 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 注入 Query 的context参数,让你在实现中直接使用其 Prisma API:

import { type Task } from "wasp/entities" import { type GetAllTasks, type GetFilteredTasks } from "wasp/server/operations" export const getAllTasks: GetAllTasks<void, Task[]> = async (args, context) => { return context.entities.Task.findMany({}) } export const getFilteredTasks: GetFilteredTasks< Pick<Task, "isDone">, Task[] > = async (args, context) => { return context.entities.Task.findMany({ where: { isDone: args.isDone }, }) }

context.entities.Task对象暴露的就是 Prisma 的 CRUD API(如findManyfindUniquecount等)。再次强调:标注 Query 是可选的,但能显著提升全栈类型安全

关于auth选项queryspec 还支持auth配置。在 examples/kitchen-sink/src/features/operations/operations.wasp.ts 中可以看到query(getNumTasks, { entities: ["Task"], auth: false })的用法——它明确关闭了该 Query 的认证要求。这一选项在 spec 映射源码 中被解构出来,用于控制 Query 是否自动注入用户认证上下文。

Query 与 Action 的异同

Query 与 Action 是 Wasp 中两个紧密相关的概念,理解它们的区别至关重要:

  1. Action 可以(且通常应该)修改服务端状态,而 Query 只允许读取状态。Wasp 进行缓存失效时依赖你遵守这一约定,因此务必遵守。
  2. Action 不需要响应式,可以直接调用;但 Wasp 也提供了useActionReact 钩子为 Action 增加额外行为(如乐观更新)。
  3. actionspec 与queryspec 基本一致,唯一的区别在于 spec 的名字

相关的缓存失效机制

由于 Wasp 使用react-query管理 Query 缓存,需要确保数据过期时失效缓存。手动失效(refetch、直接 invalidation)容易出错,因此 Wasp 提供了开箱即用的基于 Entity 的自动缓存失效:因为 Action 会修改状态而 Query 读取状态,所以每当一个使用某 Entity 的 Action 被执行时,Wasp 就会失效使用同一 Entity 的 Query 缓存

例如,ActioncreateTask与 QuerygetTasks都使用 EntityTask,执行createTask可能使getTasks的缓存结果过期,Wasp 会将其失效并触发重新拉取。这意味着 Wasp 让 Query 保持"新鲜"而无需你操心缓存失效。该机制的详细说明见 web/docs/data-model/operations/actions.md 的 "Cache Invalidation" 一节。

如果这种自动失效不够精细(可能产生不必要的更新),你可以使用react-query提供的机制;Wasp 的useAction钩子目前是唯一原生支持的手动缓存机制(乐观更新)。在 TypeScript 中,还可以通过任意 Query 的queryCacheKey属性直接获取其内部缓存 key,以便使用react-query底层 API。

API 参考

声明 Query(Specifying Queries)

声明一个 Query 后,你可以在代码任意位置(服务端或客户端)导入并使用它。例如声明为getFoo的 Query,Wasp 会生成两个同名函数:

// Use it on the client import { getFoo } from "wasp/client/operations" // Use it on the server import { getFoo } from "wasp/server/operations"

在 TypeScript 中,它还会生成一个可在服务端导入的类型:

import { type GetFoo } from "wasp/server/operations"

queryspec 的所有支持选项可以在 spec 包源码 中查看:构造函数query(fn, config?)接收实现引用fn和可选配置config,返回{ kind: "query", fn, ...config }QueryConfigentitiesauth等可选字段组成。

实现 Query(Implementing Queries)

Query 的实现是一个接收两个参数的 NodeJS 函数(需要await时可以是async函数)。由于两个参数都是位置参数,参数名可以随意命名,但约定俗成用argscontext

  1. args(类型取决于 Query)——调用 Query 时传入的数据对象(如筛选条件)。参阅上面的"使用 Query"一节了解如何传入。
  2. context(类型取决于 Query)——由 Wasp 传入的附加上下文对象,包含用户会话信息与实体信息。context.entities的用法见"在 Query 中使用 Entities",context.user的用法见 web/docs/auth/overview.md。

在 TypeScript 中,声明 Query 后 Wasp 会生成泛型类型:声明为getSomething的 Query 对应类型GetSomething,接收两个(可选)类型参数:

  1. Input——args对象的类型(Query 的输入 payload),默认值为never
  2. Output——Query 返回值的类型(Query 的输出 payload),默认值为unknown

默认值被设计为尽可能宽松;如果希望 Query 不接收/不返回任何内容,请使用void作为类型参数。

完整示例
import { app, query } from "@wasp.sh/spec" import { getFoo } from "./src/queries" with { type: "ref" } export default app({ // ... spec: [ query(getFoo, { entities: ["Foo"] }), ], })

上面的声明期望在src/queries.ts中找到命名导出getFoo。使用生成的类型GetFoo并指定输入输出:

import { type GetFoo } from "wasp/server/operations" type Foo = // ... export const getFoo: GetFoo<{ id: number }, Foo> = (args, context) => { // implementation };

这里 Query 期望接收一个含id字段(类型number)的对象作为args,并返回类型为Foo的值。

useQuery钩子

Wasp 的useQuery钩子是react-queryuseQuery钩子的薄封装,关键区别是无需提供缓存 key——Wasp 在底层代劳。它接收三个参数:

  • queryFn(必填)——Wasp 根据 Wasp 文件中的queryspec 生成的客户端 Query 函数。
  • queryFnArgs——希望传入 Query 的参数对象(payload),Query 的 NodeJS 实现会将其作为第一个位置参数接收。
  • options——一个react-queryoptions对象,用于改变某个 Query 的默认行为。若要修改全局默认值,可以在 客户端配置函数 中进行设置。

仓库实战佐证:kitchen-sink 中的完整 Query 链路

examples/kitchen-sink 是仓库内覆盖最全面的示例应用,其 operations 模块展示了 Query 的完整落地形态:

  1. 声明(operations.wasp.ts):getTasksgetNumTasksgetTaskgetOldestTask四个 Query 均声明了{ entities: ["Task"] },其中getNumTasks额外设置了auth: false
  2. 实现(queries.ts):getTasks使用satisfies GetTasks<void>让 TS 推断返回值;getTaskGetTask<Pick<Task, "id">, Task>标注输入输出,并在实现中结合context.user做权限校验(IDOR 防护);getNumTasks演示了context.entities.Task.count()的用法。
  3. 客户端响应式使用(components/Todo.tsx):const { data: tasks, isError, error: tasksError } = useQuery(getTasks)直接消费 Query 结果并处理错误状态。

这条从 Wasp 文件声明、NodeJS 实现到 React 组件消费的完整链路,正是本指南所有概念的落地验证。配套的 cacheInvalidation.test.ts 还覆盖了基于 Entity 的自动缓存失效行为,可以作为深入理解 Query 缓存语义的阅读材料。

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 20:21:04

2012-2015老Mac装最新macOS:OpenCore Legacy Patcher手把手教程

2012-2015老Mac装最新macOS&#xff1a;OpenCore Legacy Patcher手把手教程 【免费下载链接】OpenCore-Legacy-Patcher Experience macOS just like before 项目地址: https://gitcode.com/GitHub_Trending/op/OpenCore-Legacy-Patcher 2012 到 2015 年买的 MacBook Pro…

作者头像 李华
网站建设 2026/9/13 20:20:44

多传感器融合定位:从传感器特性到工程落地的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 20:20:26

Cilium Agent Pod 持续 CrashLoopBackOff 怎么排查

Cilium Agent Pod 持续 CrashLoopBackOff 怎么排查 【免费下载链接】cilium eBPF-based Networking, Security, and Observability 项目地址: https://gitcode.com/GitHub_Trending/ci/cilium 在 Kubernetes 集群中部署 Cilium 后&#xff0c;kube-system 命名空间下的 …

作者头像 李华
网站建设 2026/9/13 20:17:30

AI Agent记忆系统设计:跨会话持久化与三层存储架构

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华