TanStack Query Angular 实战:用 queryOptions 集中管理并类型安全地复用查询配置
2026/9/7 10:04:37 网站建设 项目流程

TanStack Query Angular 实战:用 queryOptions 集中管理并类型安全地复用查询配置

【免费下载链接】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 Options 展开,讲解如何在@tanstack/angular-query-experimental中用queryOptions把查询配置(queryKey+queryFn等)抽取为可复用、类型安全的共享对象,并深入源码说明其重载类型设计、queryKey数据标签机制,以及如何与injectQueryQueryClient配合完成组件内外的一致消费。读完本文,你将掌握在 Angular 应用中建立"查询配置服务层"的完整模式:从服务内集中定义,到组件响应式消费,再到组件外通过QueryClient读写缓存,全部保持编译期类型安全。

一、queryOptions 解决什么问题

在没有queryOptions之前,同一个查询的queryKeyqueryFn往往要在多个组件中重复书写;而一旦你在某处手写['post', postId]这个 key,TypeScript 无法知道它对应的是什么数据结构,getQueryDatasetQueryData等操作也就退化为unknown

queryOptions的定位就是:允许以类型安全的方式共享和复用查询配置,并且会把queryKey打上来自queryFn返回值的类型标签(源码 JSDoc 原文:"ThequeryKeywill be tagged with the type fromqueryFn",见 query-options.ts)。

值得注意的是,从源码看它的运行时实现极其简单:

// packages/angular-query-experimental/src/query-options.ts export function queryOptions(options: unknown) { return options }

它是一个纯粹的类型层函数,运行时原样返回传入对象(对应测试断言expect(queryOptions(object)).toBe(object),见 query-options.test.ts),没有任何运行时开销。它的价值全部体现在 TypeScript 的类型重载与queryKey标签上,下文第四节展开。

二、服务模式:在 QueriesService 中集中定义查询配置

指南给出的核心示例是把查询配置集中到一个@Injectable服务中,仓库中的示例工程 query-options-from-a-service 完整实现了该模式(运行方式见其 README:pnpm installpnpm start)。服务实现见 queries-service.ts:

import { Injectable, inject } from '@angular/core' import { lastValueFrom } from 'rxjs' import { queryOptions } from '@tanstack/angular-query-experimental' import { HttpClient } from '@angular/common/http' export interface Post { id: number title: string body: string } @Injectable({ providedIn: 'root', }) export class QueriesService { private readonly http = inject(HttpClient) post(postId: number) { return queryOptions({ queryKey: ['post', postId], queryFn: () => { return lastValueFrom( this.http.get<Post>( `https://jsonplaceholder.typicode.com/posts/${postId}`, ), ) }, }) } posts() { return queryOptions({ queryKey: ['posts'], queryFn: () => lastValueFrom( this.http.get<Array<Post>>( 'https://jsonplaceholder.typicode.com/posts', ), ), }) } }

几个要点:

  • 参数化 keypost(postId)以方法参数构造queryKey,同一个配置工厂可以服务任意文章 ID,key 的粒度与参数一一对应,天然获得按 ID 的缓存隔离。
  • RxJS 到 Promise 的桥接HttpClient返回ObservablelastValueFrom将其转换为Promise<Post>,符合queryFn的约定。
  • 类型推断自动完成http.get<Post>(...)的泛型参数经queryFn一路推断到queryOptions的返回类型,post(1)返回的配置对象中queryKey已被标注为"数据结构为Post",调用方无需任何显式类型标注。

三、组件内消费:input.required + injectQuery

指南示例的第二部分展示了组件如何消费服务中的配置,并顺带演示了与QueryClient的交互:

// 文档示例(docs/framework/angular/guides/query-options.md) postId = input.required({ transform: numberAttribute, }) queries = inject(QueriesService) postQuery = injectQuery(() => this.queries.post(this.postId())) queryClient.query(this.queries.post(23)).catch(noop) queryClient.setQueryData(this.queries.post(42).queryKey, newPost)

仓库示例中 post.component.ts 是这份代码的完整落地:

@Component({ changeDetection: ChangeDetectionStrategy.OnPush, selector: 'post', templateUrl: './post.component.html', imports: [RouterLink], }) export default class PostComponent { private readonly queries = inject(QueriesService) readonly postId = input.required({ transform: numberAttribute, }) readonly postQuery = injectQuery(() => this.queries.post(this.postId())) }

这里有三个值得展开的细节:

  1. injectQuery接收的是函数() => this.queries.post(this.postId())。从 inject-query.ts 的 JSDoc 可以看到,传入的函数"会在响应式上下文中运行(will be run in the reactive context)",类似于computed:当postId信号变化时,工厂函数重新求值,queryKey随之变化,查询自动切换到新 key 并复用缓存。因此input信号必须带()调用(文档示例中的this.postId缺调用,请以示例工程为准)。
  2. input.required({ transform: numberAttribute }):模板传入的是字符串属性,numberAttribute转换器把它变成数字后进入queryKey,避免['post', '1']['post', 1]这类 key 不一致问题。
  3. 结果全是信号injectQuery返回的CreateQueryResultdataisLoadingerror等字段均为Signal(由 types.ts 中的MapToSignals映射实现),模板可直接写postQuery.data(),配合OnPush变更检测策略。

列表组件 posts.component.ts 则展示了最简单的消费形态——injectQuery(() => this.queries.posts()),一行即完成订阅,同时inject(QueryClient)表明缓存客户端本身也可注入,供组件外场景使用。

四、源码剖析:三重载与 queryKey 数据标签

queryOptions之所以能做到"零运行时成本却全程类型安全",关键在于 query-options.ts 中基于initialDataqueryFn形态拆分的三个重载:

重载类型适用条件关键约束
DefinedInitialDataOptions提供了非 undefinedinitialDataqueryFn变为可选;调用方拿到"defined"结果,data信号不会是undefined
UnusedSkipTokenOptionsqueryFn使用了skipToken(禁用查询)queryFn被收窄为排除SkipToken的真实函数;结果dataunknown
UndefinedInitialDataOptions常规场景queryFn必填;initialData可以是值、函数或undefined(函数形式允许返回undefined,用于"条件性初始数据")

三个重载的返回值都叠加了同一个标签类型:

DefinedInitialDataOptions<...> & QueryKeyWithDataTag<TQueryKey, TQueryFnData, TError>

QueryKeyWithDataTag通过query-coredataTagSymbol把一个"不可见"的符号属性挂在queryKey上,其值就是queryFn的返回数据结构。类型测试文件 query-options.test-d.ts 直接验证了这一机制:

const { queryKey: tagged } = queryOptions({ queryKey: key, queryFn: () => Promise.resolve(5), }) assertType<number>(tagged[dataTagSymbol])

正是这个标签让第二节的"组件外读写缓存"成为可能:

const { queryKey } = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), }) const data = queryClient.getQueryData(queryKey) // ^? number | undefined queryClient.setQueryData(queryKey, (prev) => prev) // prev: number | undefined

类型测试进一步确认:getQueryData返回number | undefined(L163-L174);setQueryData传入错误类型的值(如字符串'5')会直接触发编译错误(L192-L209)。此外,若配置中没有queryFn,标签会退化为unknown(L143-L150),即未声明数据来源的 key 只能以unknown访问缓存,这是有意为之的保守设计。

queryOptions返回的配置对象还可以直接喂给QueryClient的实例方法,类型测试覆盖了这些用法(见 query-options.test-d.ts):

  • new QueryClient().fetchQuery(options)Promise<number>
  • new QueryClient().query(options)Promise<number>,且select生效(select: (data) => data.toString()时返回Promise<string>
  • 配合enabled: falsequeryFn: skipToken时类型行为正确(skipToken场景下结果为Promise<unknown>

这说明"配置即对象"的抽象在组件(injectQuery)与组件外(fetchQuery/query/getQueryData/setQueryData)是统一且可互换的。

五、类型推断与配置覆盖:在共享配置上扩展 select

指南的第二个示例展示了共享配置的一个重要特性:调用点可以展开并覆盖字段,且类型推断仍然成立

// Type inference still works, so query.data will be the return type of select instead of queryFn queries = inject(QueriesService) query = injectQuery(() => ({ ...groupOptions(1), select: (data) => data.title, }))

由于queryOptions返回的就是普通选项对象(运行时return options),在调用点用对象展开合并再传入injectQuery是完全合法的;select的类型会沿着重载传导——query.data信号的类型是select的返回类型(data.titlestring),而不是queryFn的原始类型。这一推断链有类型测试佐证:

const options = queryOptions({ queryKey: ['key'], queryFn: () => Promise.resolve(5), select: (data) => data.toString(), }) const data = new QueryClient().query(options) assertType<Promise<string>>(data)

(见 query-options.test-d.ts)

需要注意的边界是:queryKey上的数据标签仍然基于queryFn的返回类型而非select的类型,类型测试should tag the queryKey with the result type of the QueryFn if select is used(L152-L161)确认标签值为number而非string——即缓存层存的始终是原始数据,select只影响观察层。

另外,CreateQueryOptions的基础类型继承自QueryObserverOptions(去掉suspense字段,见 types.ts),因此staleTimerefetchIntervalenabledretryplaceholderData等所有观察者选项都可以在服务配置中或调用点覆盖时直接使用;同时类型测试should not allow excess properties确认了选项对象会进行精确的属性检查,不会出现拼写错误的静默忽略。

六、实践建议与相关文件

结合上述源码与测试,给出几条可直接落地的实践建议:

  • 一个服务按实体域聚合配置:如QueriesService.post(postId)/posts()的写法,key 构造与请求逻辑同址维护,避免 key 漂移。
  • 组件内用injectQuery(() => service.method(deps)):把输入信号作为工厂参数传入,保证响应式重取;组件外需要"触发一次查询并拿到 Promise"时用queryClient.query(options)fetchQuery(options),并记得.catch(noop)之类的错误处理(文档示例即演示了queryClient.query(this.queries.post(23)).catch(noop))。
  • 预热缓存走setQueryData(options.queryKey, ...):带标签的queryKey保证写入值与queryFn返回类型一致(文档示例queryClient.setQueryData(this.queries.post(42).queryKey, newPost))。
  • 适用前提:本文基于@tanstack/angular-query-experimental包的当前仓库实现,示例工程使用 Angular 的injectinput等新 API;具体版本能力以仓库中 packages/angular-query-experimental/package.json 声明为准。

本文涉及的关键文件:

  • 指南原文:docs/framework/angular/guides/query-options.md
  • 核心实现:packages/angular-query-experimental/src/query-options.ts、packages/angular-query-experimental/src/types.ts、packages/angular-query-experimental/src/inject-query.ts、导出入口 packages/angular-query-experimental/src/index.ts
  • 测试:packages/angular-query-experimental/src/tests/query-options.test.ts、packages/angular-query-experimental/src/tests/query-options.test-d.ts
  • 示例工程:examples/angular/query-options-from-a-service(含 queries-service.ts、post.component.ts、posts.component.ts)

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

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

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

立即咨询