Solid Query 查询函数(Query Functions)实战指南:Promise 契约、错误处理与 QueryFunctionContext
2026/9/9 12:35:19 网站建设 项目流程

Solid Query 查询函数(Query Functions)实战指南:Promise 契约、错误处理与 QueryFunctionContext

【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query

本篇指南围绕 TanStack Query 在 Solid 生态的官方实现@tanstack/solid-query(本仓库 packages/solid-query)中的Query Functions概念展开。查询函数是useQuery获取数据的核心引擎,理解"函数必须返回 Promise、要么 resolve 数据要么抛错"这一契约,并掌握QueryFunctionContext的用法,是正确写出可缓存、可取消、可重试查询的基础。读完本文你将能写出规范的查询函数、优雅地处理各类请求错误,并利用queryKeysignal等上下文信息实现可复用的查询逻辑。Solid 与 React 版本的此篇指南共享同一份原始素材(见 docs/framework/react/guides/query-functions.md),但本文将完全围绕 Solid Query 的函数式选项语法(options 以函数形式传入)与 SolidJS 响应式特性展开。

一、查询函数的基本规则:一个"Promise 契约"

在 Solid Query 中,query function(查询函数)可以是任何"返回 Promise 的函数",其返回值遵循两个硬性约定:

  1. 成功时 resolve 数据——Promise 成功解析出来的值会写入查询缓存,并成为data
  2. 失败时抛错或 reject——函数抛出的任何异常都会被捕获,持久化到该查询的error状态上。

此外,成功解析的值不能是undefined。解析为undefined的查询会被当作失败处理(历史上undefined是成功查询的非法缓存值)。如果你确实想缓存"空"这一成功结果,请显式 resolvenull

从类型定义上看,这一契约被直接编码在核心层(packages/query-core/src/types.ts#L102-L106):

export type QueryFunction< T = unknown, TQueryKey extends QueryKey = QueryKey, TPageParam = never, > = (context: QueryFunctionContext<TQueryKey, TPageParam>) => T | Promise<T>

也就是说,查询函数既可以同步返回数据T),也可以异步返回 PromisePromise<T>),最终都会由底层以 Promise 语义统一处理。

二、Solid Query 的独特语法:options 以 Accessor 函数传入

在使用示例前,必须先强调 Solid Query 与 React Query 的一个关键差异:useQuery接收的不是一个普通对象,而是一个返回 options 对象的函数(Accessor)。这与 React 版useQuery({ queryKey, queryFn })的写法不同。

看 useQuery.ts 的实现即可理解原因:options 被包装进createMemo(() => options()),因此其中引用到的信号(signal,即 SolidJS 响应式状态)发生变化时,查询选项会自动重建并触发重取(refetch)。这是 SolidJS 细粒度响应式在查询层面的体现。

import { useQuery } from '@tanstack/solid-query' const todoId = 1 useQuery(() => ({ queryKey: ['todos'], queryFn: fetchAllTodos })) useQuery(() => ({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId), })) useQuery(() => ({ queryKey: ['todos', todoId], queryFn: async () => { const data = await fetchTodoById(todoId) return data }, })) useQuery(() => ({ queryKey: ['todos', todoId], queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]), }))

以上四种写法都是合法且等价的配置:直接引用函数、闭包捕获参数、async/await包装、以及从queryKey上下文解构参数。

在真实项目中(参考 examples/solid/basic/src/index.tsx#L26-L34),典型用法是把queryKey作为依赖跟踪信号,让todoId变化时自动重新拉取:

import { useQuery } from '@tanstack/solid-query' import { createSignal } from 'solid-js' function createPost(postId: () => number) { return useQuery(() => ({ queryKey: ['post', postId()], queryFn: () => getPostById(postId()), enabled: !!postId(), })) }

注意,若queryFn中通过闭包引用了外部参数,而queryKey没有同步纳入这些参数,底层将无法正确识别查询变化。关于 key 的设计原则可参考 Query Keys 指南,关于响应式入参的更多细节可结合 Queries 基础指南 阅读。

三、错误抛出与处理:throw 与 rejected Promise

要让 Solid Query 判断一个查询出错,查询函数必须主动抛出异常(throw)或返回一个 rejected 的 Promise。任何在查询函数内部被抛出的错误,都会进入查询的error状态,并可通过解构error读取:

const todosQuery = useQuery(() => ({ queryKey: ['todos', todoId], queryFn: async () => { if (somethingGoesWrong) { throw new Error('Oh no!') } if (somethingElseGoesWrong) { return Promise.reject(new Error('Oh no!')) } return data }, }))

在 UI 层,@tanstack/solid-query返回的查询结果对象是响应式的 store,你可以直接基于status/isError/isPending等字段做渲染分支(见 examples/solid/basic/src/index.tsx#L43-L77 中的<Switch>/<Match>用法):

const state = createPosts() // ... <Switch> <Match when={state.status === 'pending'}>Loading...</Match> <Match when={state.status === 'error'}> <span>Error: {(state.error as Error).message}</span> </Match> <Match when={state.data !== undefined}> <For each={state.data}>{(post) => <p>{post.title}</p>}</For> </Match> </Switch>

值得留意的是:错误必须由查询函数主动抛出。Solid Query 无法替你把"非 2xx 的 HTTP 响应"翻译成异常——这取决于你使用的请求工具本身,这正是下一节要解决的问题。

四、配合fetch使用:处理默认不抛错的客户端

axiosgraphql-request等工具库通常会对不成功的 HTTP 调用自动抛错;但原生fetch默认不会对 4xx / 5xx 响应抛错,它只会在网络层面失败(如断网、DNS 解析失败)时 reject。因此当你使用fetch时必须自己手动抛出错误。一种常见的写法是检查response.ok

useQuery(() => ({ queryKey: ['todos', todoId], queryFn: async () => { const response = await fetch('/todos/' + todoId) if (!response.ok) { throw new Error('Network response was not ok') } return response.json() }, }))

这条规则很容易被忽略:如果漏掉response.ok检查,接口返回 404 时查询仍会被当作成功,数据层将拿到空内容而 UI 层毫无感知。把"HTTP 状态码错误"显式转换为抛错,才能让 Solid Query 的retryerror、错误边界等机制完整生效。请求重试策略详见 Query Retries 指南(若需引用可参见 solid 同名目录),此处不再展开。

五、Query Function Variables:QueryKey 如何"顺路"进入查询函数

查询键(queryKey)不仅用于唯一标识所获取的数据,还会作为QueryFunctionContext的一部分被自动传入查询函数。这意味着你可以把查询函数抽离成独立模块,而不必依赖闭包捕获外部变量:

function Todos(props) { const todosQuery = useQuery(() => ({ queryKey: ['todos', { status: props.status, page: props.page }], queryFn: fetchTodoList, })) } // 直接在查询函数里解构 key、status 和 page! function fetchTodoList({ queryKey }) { const [_key, { status, page }] = queryKey return new Promise() }

这一模式的工程意义在于"查询函数与组件解耦":

  • 组件侧只需声明queryKey,把它当作 UI 参数的"投影";
  • 查询函数侧通过结构一致的queryKey数组还原出参数,天然可单独测试、可复用;
  • 由于queryKey同时参与缓存寻址,这样写还能保证参数与缓存键永不脱节。

配合 default-query-function.md 中介绍的默认查询函数(可以做到完全不传queryFn),上述解耦价值会被进一步放大。

六、QueryFunctionContext 详解

QueryFunctionContext传给每个查询函数的唯一参数对象,也是"查询函数变量"机制的底层来源。其构成字段如下:

字段类型说明
queryKeyQueryKey本次查询的键,即定义查询时传入的queryKey,详见 Query Keys 指南
clientQueryClient当前使用的QueryClient实例,API 参考见 QueryClient
signalAbortSignal由 TanStack Query 提供的AbortSignal实例,可用于实现查询取消
metaRecord<string, unknown> \| undefined可选字段,可在其中填充关于该查询的附加信息

在核心层的类型定义(packages/query-core/src/types.ts#L144-L171)中,上述字段被明确建模为QueryFunctionContext<TQueryKey, TPageParam>:常规查询时pageParamdirection以可选形式存在;而在泛型参数TPageParam被具体化(即无限查询场景)时,则变为必选字段。

此外,Infinite Queries(无限查询)的查询函数还会额外收到两个字段:

  • pageParam: TPageParam—— 获取当前页所使用的页码参数;
  • direction: 'forward' | 'backward'—— 当前页抓取的方向;
    • 已废弃(deprecated):如需感知抓取方向,官方建议在getNextPageParam/getPreviousPageParam返回的pageParam中自行带上方向信息,而不要依赖此字段。

基于signal字段,可以实现真正的请求可取消性:例如把context.signal透传给原生fetch,组件卸载或缓存失效时请求即被中止(详见 Query Cancellation 指南)。client字段则让你在查询函数内也能调用client.getQueryData()等命令式 API 做数据联动。

七、源码级延伸:查询函数在 Solid Query 内部如何被驱动

理解上述 API 后,再看一眼@tanstack/solid-query的实现能帮你更准确地建立心智模型。

1. 选项作为 Accessor 求值。useQuery.ts 的实体会把options()结果包装为createMemo后交给useBaseQuery,这意味着 options 内的每个响应式依赖变化都会驱动 observer 更新选项并重取:

export function useQuery<TQueryFnData, TError, TData, TQueryKey extends QueryKey>( options: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: Accessor<QueryClient>, ) { return useBaseQuery(createMemo(() => options()), QueryObserver, queryClient) }

顺带一提,Solid 版 options 类型即Accessor<QueryOptions<...>>(见 packages/solid-query/src/types.ts#L60-L65),这正是"传函数而非对象"语法约束的类型来源。

2. Promise 被接入 SolidJS 的createResource在 useBaseQuery.ts 中,查询函数产生的异步结果被包装进createResourcedata因此具备 SolidJS 资源的响应式与 Suspense 语义(这也是 Solid 版没有独立suspense开关、data会自动触发挂起的原因)。查询结果最终以一个 Proxy store 的形式返回,读取state.data时实际走的是 resource 通道。

3. 服务端渲染下的默认差异。同文件 useBaseQuery.ts 表明,在服务端(isServer)会把默认retry强制置为falsethrowOnError置为true——查询失败会直接抛错给 SSR 流,而不是无限重试拖垮首屏。此外客户端默认关闭structuralSharing(useBaseQuery.ts),改用 store 的reconcile机制(配合可选的structuredClone)做数据合并,这是为 SolidJS 细粒度更新做的适配。

如果只关心结果状态,而不关心底层驱动细节,直接使用state.status/state.isPending/state.isError/state.data等字段即可——返回对象是响应式的,模板中直接读取即可自动更新。

八、与其他概念的关系小结

查询函数并非孤立概念,它与以下主题紧密咬合,建议按需延伸阅读(均为本仓库 Solid 框架指南,路径统一从仓库根目录出发):

  • Queries(查询基础)——useQuery的完整返回值与status/fetchStatus状态机;
  • Query Keys(查询键)——queryKey的结构化设计规范;
  • Query Cancellation(查询取消)——利用QueryFunctionContext.signal实现请求中止;
  • Infinite Queries(无限查询)——pageParam与方向字段的实战场景;
  • Default Query Function(默认查询函数)——当多个查询共享同一套取数逻辑时的收口方式;
  • Query Retries(查询重试)——查询函数抛错后的重试语义。

掌握本文的 Promise 契约、主动抛错原则与QueryFunctionContext之后,你便拥有了写出健壮、可复用、可取消查询函数所需的全部核心知识。

【免费下载链接】query🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query.项目地址: https://gitcode.com/GitHub_Trending/qu/query

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询