news 2026/9/7 14:36:41

Angular Query(TanStack Query)查询函数详解:queryFn 的写法、错误处理与 QueryFunctionContext 深入解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Angular Query(TanStack Query)查询函数详解:queryFn 的写法、错误处理与 QueryFunctionContext 深入解析

Angular Query(TanStack Query)查询函数详解:queryFn 的写法、错误处理与 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 的 Angular 版指南 Query Functions 展开,系统讲解 Angular 生态下injectQueryqueryFn的合法写法、错误抛出机制、fetch/HttpClient等不默认抛错的客户端如何适配,以及QueryFunctionContext的完整字段。文中所有机制均结合 query-core 源码 与 angular-query-experimental 包源码 逐一印证,读完后你将掌握在 Angular 中编写可复用、可取消、错误行为可控的查询函数所需的完整知识。

一、什么是查询函数:返回 Promise 的任意函数

查询函数(query function)可以是任何返回 Promise 的函数。这个 Promise 只有两种合法结局:

  • resolve 出数据(成功),或
  • throw 一个错误(失败)。

QueryFunction的类型定义印证了这一点,它就是一个接收上下文、返回数据或 Promise 的函数签名,见 QueryFunction 类型定义:

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

一个关键约束:成功时 resolve 的值不能是undefined。resolve 出undefined的查询会被视为失败。如果你的业务确实需要"成功但什么都不存",请 resolvenull

这一约束在核心实现中有明确的运行时守卫。当重试器(retryer)拿到的数据为undefined时,源码会先打印开发期警告、随后直接抛错,见 Query 数据获取实现:

const data = await retryer.start() // this is more of a runtime guard if (data === undefined) { if (process.env.NODE_ENV !== 'production') { console.error( `Query data cannot be undefined. Please make sure to return a value other than undefined from your query function. Affected query key: ${this.queryHash}`, ) } throw new Error(`${this.queryHash} data is undefined`) } this.setData(data)

也就是说,把null当作"合法的空数据"是官方语义,undefined则会被转换为查询失败,进入错误处理与重试流程。

二、Angular 中 queryFn 的常见写法

在 Angular 中,查询通过injectQuery注入。下面四种配置方式全部合法(引自 Angular Query Functions 指南):

// 1. 直接传入一个具名函数 injectQuery(() => ({ queryKey: ['todos'], queryFn: fetchAllTodos })) // 2. 用闭包捕获 todoId injectQuery(() => ({ queryKey: ['todos', todoId], queryFn: () => fetchTodoById(todoId), })) // 3. 用 async 函数包裹并显式返回数据 injectQuery(() => ({ queryKey: ['todos', todoId], queryFn: async () => { const data = await fetchTodoById(todoId) return data }, })) // 4. 不依赖闭包,直接从 queryFn 上下文的 queryKey 中取参数 injectQuery(() => ({ queryKey: ['todos', todoId], queryFn: ({ queryKey }) => fetchTodoById(queryKey[1]), }))

这几种写法对应不同的复用诉求:前两种适合"数据函数已经写好了"的场景;第三种适合需要在取数前后插入额外逻辑(如转换、鉴权、日志)的场景;第四种把queryKey作为唯一数据来源,是把 queryFn 抽离为独立、可复用函数的前提——因为此时函数不再依赖组件内的任何闭包变量。

需要注意 Angular 特有的前提:injectQuery必须运行在注入上下文中。源码实现通过assertInInjectionContext做校验,并经由runInInjectionContext执行,见 injectQuery 实现:

export function injectQuery( injectQueryFn: () => CreateQueryOptions, options?: InjectQueryOptions, ) { !options?.injector && assertInInjectionContext(injectQuery) return runInInjectionContext(options?.injector ?? inject(Injector), () => createBaseQuery(injectQueryFn, QueryObserver), ) as unknown as CreateQueryResult }

三、错误处理与抛出错误

要让 TanStack Query 判定一次查询失败,查询函数必须 throw或返回一个rejected Promise。函数内抛出的任何错误都会持久化在查询的error状态中(对应结果对象上的error字段):

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

在 Angular 包中,这个错误最终如何被消费也值得看一眼:createBaseQuery 实现 中,查询结果订阅在ngZone.runOutsideAngular中执行,只有当state.isError && !state.isFetchingthrowOnError判定为真时,才会通过ngZone.onError.emit(state.error)上报并重新抛出,否则错误仅通过resultFromSubscriberSignal.set(state)写入响应式信号——这正是"错误既存在于查询状态、又可通过throwOnError决定是否外抛"这一行为的底层支撑。

四、适配fetch等不默认抛错的客户端

大多数 HTTP 库(如axiosgraphql-request)会对非 2xx 响应自动抛错,但原生的fetch不会——404、500 都只是"成功的 Promise"。这种情况下你需要自己把失败转换为错误:

injectQuery(() => ({ 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() }, }))

在 Angular 项目里,HttpClient是另一条主流路径:它的响应是 Observable,而 TanStack Query 是 Promise 体系,因此需要用 RxJS 的lastValueFromfirstValueFrom做转换:

@Component({ // ... }) class ExampleComponent { private readonly http = inject(HttpClient) readonly query = injectQuery(() => ({ queryKey: ['repoData'], queryFn: () => lastValueFrom( this.http.get('https://api.github.com/repos/tanstack/query'), ), })) }

HttpClient的额外收益还包括:与 Angular 依赖注入集成的拦截器(鉴权头、日志)、PendingTasks感知(利于 Zoneless 单测与 SSR 稳定性判断),以及 SSR 下的服务端请求缓存。选型对比可参考 HttpClient and other data fetching clients 中的完整分析。

五、查询函数变量:从 queryKey 中提取参数

queryKey不仅用于唯一标识数据,还会作为QueryFunctionContext的一部分自动传入查询函数。这使得在需要时可以把 queryFn 提取为独立函数,而不必通过闭包传递参数:

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

这个模式在 Angular 官方示例(query-options-from-a-service)中有完整演示:把 query key 与 queryFn 的组装收敛到 service 中,组件只负责注入与渲染,非常适合"一个查询函数同时服务多个组件"的场景。

QueryFunctionContext 字段全解

QueryFunctionContext是每次调用查询函数时传入的对象。从 类型定义 看,普通查询包含:

  • queryKey: QueryKey:本次查询的键,见 Query Keys 指南;
  • client: QueryClient:发起本次查询的客户端实例,见 QueryClient 参考;
  • signal: AbortSignal:由 TanStack Query 提供的中止信号,用于 查询取消;
  • meta: Record<string, unknown> | undefined:可选的自定义附加信息。

此外,Infinite Queries 的查询函数额外收到:

  • pageParam: TPageParam:用于获取当前页的页参;
  • direction: 'forward' | 'backward'已废弃。若需感知翻页方向,官方建议把方向信息编入getNextPageParam/getPreviousPageParam生成的pageParam中(类型定义中的@deprecated注释与 deprecation 标记 一致)。

signal 的实现细节:惰性提供、按需消费

从源码结构看,signal并不是无条件创建的普通属性。Query 数据获取实现 中,AbortController在每次 fetch 前新建,signal通过Object.defineProperty以 getter 形式挂到上下文上;只有当查询函数真正读取signal,内部才会把#abortSignalConsumed置为true

const abortController = new AbortController() // Adds an enumerable signal property to the object that // which sets abortSignalConsumed to true when the signal is read. const addSignalProperty = (object: unknown) => { Object.defineProperty(object, 'signal', { enumerable: true, get: () => { this.#abortSignalConsumed = true return abortController.signal }, }) } const createQueryFnContext = (): QueryFunctionContext<TQueryKey> => { const queryFnContext = { client: this.#client, queryKey: this.queryKey, meta: this.meta, } addSignalProperty(queryFnContext) return queryFnContext }

这一机制的实用含义是:如果你把signal传给fetchHttpClient的取消链路,查询在重试、被更优先的 fetch 抢占等场景下被取消时,底层 retryer 的 onCancel 钩子 会调用abortController.abort(),你的 HTTP 请求会随之中断。同时,如果配置了persister(如 query-sync-storage-persister 这类持久化方案),查询函数会被包装,queryFnqueryFnContextQuery实例三者都会传给 persister,见 persister 分支。

六、Angular 特有机制:选项函数在响应式上下文中运行

injectQuery的第一个参数是一个返回选项的函数,而非选项对象本身——这是 Angular 版与 React 版最显眼的差异,也是 queryFn 编写的重要背景:

  • 该函数与 Angular 的computed类似,运行在响应式上下文中。选项中读取的所有 signal 都会建立依赖:依赖值变化时选项会被重新求值,查询自动以新选项重新订阅;
  • 因此queryKey中可以直接调用todoId()status()等信号,key 变化即触发新查询,无需手动refetch

这一行为在 createBaseQuery 实现 中清晰可见:optionsFn()的结果被包进computed,并经queryClient.defaultQueryOptions应用QueryClient层面的默认值(包括全局默认queryFn),生成defaultedOptionsSignal;随后QueryObserver基于该信号创建,effect在信号变化时调用observer.setOptions同步新选项。

由此还引出一个实用能力:如果通过QueryClient.defaultOptions.queries.queryFn设置了默认查询函数,injectQuery甚至可以省略 queryFn,只给queryKey——选项函数响应式求值后,全局 queryFn 会自动补齐。相关实践见 Default Query Function 指南。

另外从 createBaseQuery 可以看到 Angular 的PendingTasks集成:订阅回调里fetchStatus === 'fetching'时调用pendingTasks.add()登记待办任务,fetchStatus回到idle时解除。这意味着你在queryFn中发起的每次请求都会让 Angular(单测、SSR、provideClientHydration等场景)感知到"应用尚未稳定",queryFn的耗时与结果正确性会直接影响应用的稳定性判断。

七、小结

  • 查询函数 = 返回 Promise 的任意函数:resolve 出数据(不可为undefined,空值请用null)或 throw 错误;
  • 合法写法包括具名函数、闭包、async 包裹、从queryKey取参四种,最后一种让 queryFn 可完全抽离复用;
  • fetch不默认抛错,需自行判断response.ok;Angular 的HttpClient则需lastValueFrom/firstValueFrom转 Promise;
  • QueryFunctionContext提供queryKeyclientsignalmeta,Infinite Query 另加pageParamdirection已废弃);
  • Angular 版的选项函数运行在响应式上下文中,signal 依赖变化会自动驱动查询重新求值与订阅,同时请求周期会接入 Angular 的 PendingTasks 体系。

相关文档可继续延伸阅读:Query Functions(React 参考版本)、查询取消、Query Options 与 Infinite Queries。

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

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

Three.js实现3D可视化机房:从建模到交互的性能优化实战

简介&#xff1a;基于Three.js的3D可视化机房项目&#xff0c;是一份面向Web前端与三维可视化开发者的实战源码。项目通过第一人称视角在虚拟机房中自由漫游&#xff0c;查看设备布局与运行状态&#xff0c;适用于智慧园区、数据中心可视化运维等真实业务场景。压缩包共176个文…

作者头像 李华
网站建设 2026/9/7 14:34:58

BP-PID神经网络控制器Simulink仿真与代码实现

简介&#xff1a;面向需要将神经网络与PID控制结合的工程师和研究人员&#xff0c;这份资源提供了完整的BP-PID实现&#xff0c;包括Simulink模型、Matlab脚本、说明文档和示意图。其中核心Slx模型展示了BP网络与PID控制器的接口搭建&#xff0c;M脚本用于训练和参数传递&#…

作者头像 李华
网站建设 2026/9/7 14:29:22

抽奖系统技术实现:从概率算法到前后端架构详解

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

作者头像 李华
网站建设 2026/9/7 14:28:20

Notepad++高效文本排版技巧:正则、列编辑与宏的实战应用

1. 内容整体设计与思路拆解先说个现象&#xff1a;很多人电脑里装了Notepad&#xff0c;但只拿它当“比记事本能多开几个标签页”的替代品。真正遇到文本排版需求&#xff0c;比如从网页上复制了一段带格式的文章、从PDF导出了乱成一团的文字、手头有一份几千行的日志需要对齐整…

作者头像 李华
网站建设 2026/9/7 14:27:23

深入理解TCP拥塞控制:从慢启动到CUBIC与BBR核心原理

1. 从“连得上”到“跑得快”&#xff1a;为什么TCP拥塞控制值得你花时间研究做网络开发这些年&#xff0c;我见过太多人把TCP调优等同于“改改缓冲区大小”或者“把超时时间调大一点”。真到线上出问题的时候——比如文件传输突然变慢、视频卡顿、高并发下延迟飙升——才发现自…

作者头像 李华
网站建设 2026/9/7 14:26:09

ant-design Affix target 属性实战:让固钉组件跟随任意滚动容器

ant-design Affix target 属性实战&#xff1a;让固钉组件跟随任意滚动容器 【免费下载链接】ant-design An enterprise-class UI design language and React UI library 项目地址: https://gitcode.com/GitHub_Trending/an/ant-design 本文围绕 ant-design 中 Affix 组…

作者头像 李华