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 生态下injectQuery中queryFn的合法写法、错误抛出机制、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.isFetching且throwOnError判定为真时,才会通过ngZone.onError.emit(state.error)上报并重新抛出,否则错误仅通过resultFromSubscriberSignal.set(state)写入响应式信号——这正是"错误既存在于查询状态、又可通过throwOnError决定是否外抛"这一行为的底层支撑。
四、适配fetch等不默认抛错的客户端
大多数 HTTP 库(如axios、graphql-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 的lastValueFrom或firstValueFrom做转换:
@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传给fetch或HttpClient的取消链路,查询在重试、被更优先的 fetch 抢占等场景下被取消时,底层 retryer 的 onCancel 钩子 会调用abortController.abort(),你的 HTTP 请求会随之中断。同时,如果配置了persister(如 query-sync-storage-persister 这类持久化方案),查询函数会被包装,queryFn、queryFnContext与Query实例三者都会传给 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提供queryKey、client、signal、meta,Infinite Query 另加pageParam(direction已废弃);- 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),仅供参考