首页
/ TanStack Query Angular 默认 queryFn 实战:用 defaultOptions 为 injectQuery 省去逐条声明

TanStack Query Angular 默认 queryFn 实战:用 defaultOptions 为 injectQuery 省去逐条声明

2026-09-05 09:58:22作者:彭桢灵Jeremy

本篇技术指南基于 TanStack Query(Angular 适配器 @tanstack/angular-query-experimental)的 default-query-function 指南,讲解如何通过 QueryClientdefaultOptions.queries.queryFn 注册一个全局默认查询函数,从而让 injectQuery 只需传入 queryKey 即可完成数据获取。读完本文,你将掌握完整可运行的示例写法,并能在源码层面理解默认值是如何与每次调用的 options 合并、覆盖的。

核心思路:把「怎么取数据」声明一次,把「取什么」留给组件

在典型的 REST 应用里,绝大多数查询的差异只在于 URL 路径(即 queryKey),而请求方式(axios/fetch 实例、认证头、错误归一化、数据解包)是高度一致的。如果每个组件都写一遍 queryFn: () => axios.get(...).then(r => r.data),会产生大量重复代码,且请求层难以统一替换(比如全局换 baseURL、统一注入 token)。

TanStack Query 的解法就是「默认查询函数」(Default Query Function):在创建 QueryClient 时通过 defaultOptions.queries.queryFn 提供一个 QueryFunction,之后所有查询在自身未显式声明 queryFn 时,都会回落到这个默认实现,并且 queryFn 能收到完整的 QueryFunctionContext(含 queryKey),因此可以根据 key 动态拼接请求路径。

完整示例:从定义到组件使用

下面完整继承官方指南中的示例代码,覆盖「定义默认函数 → 注册到客户端 → 接入 Angular 应用 → 组件中只传 key」四个环节。

定义并注册 defaultQueryFn

// Define a default query function that will receive the query key
const defaultQueryFn: QueryFunction = async ({ queryKey }) => {
  const { data } = await axios.get(
    `https://jsonplaceholder.typicode.com${queryKey[0]}`,
  )
  return data
}

// provide the default query function to your app with defaultOptions
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      queryFn: defaultQueryFn,
    },
  },
})

要点说明:

  • 默认函数签名是 QueryFunction:接收一个上下文对象,其中最常用的是 queryKey。示例中把 queryKey[0] 直接当作 API 路径(如 /posts/posts/1)拼接到 jsonplaceholder 的 baseURL 上,这是一种「key 即路由」的约定;
  • defaultOptions.queries 是全局查询默认值的挂载点,除了 queryFn 还可以放 staleTimeretry 等(本文聚焦 queryFn);
  • QueryClient 构造器会把传入的 defaultOptions 保存下来(config.defaultOptions || {}),后续所有 defaultQueryOptions(...) 调用都从这里读取,见 QueryClient 构造函数

把客户端接入 Angular 应用

bootstrapApplication(MyAppComponent, {
  providers: [provideTanStackQuery(queryClient)],
})

provideTanStackQuery 内部通过 provideQueryClientQueryClient 注册为可注入的单例,并在 injector 销毁时调用 client.unmount()、创建时调用 client.mount(),源码见 providers.ts。它也支持传入 InjectionToken 以支持懒加载路由单独挂载 Query,并可通过 withDevtools() 启用开发者工具(文档见 providers.ts 的 JSDoc)。

组件中只传 queryKey

export class PostsComponent {
  // All you have to do now is pass a key!
  postsQuery = injectQuery<Array<Post>>(() => ({
    queryKey: ['/posts'],
  }))
  // ...
}

export class PostComponent {
  // You can even leave out the queryFn and just go straight into options
  postQuery = injectQuery<Post>(() => ({
    enabled: this.postIdSignal() > 0,
    queryKey: [`/posts/${this.postIdSignal()}`],
  }))
  // ...
}

两个细节值得注意:

  1. injectQuery 接收一个 options 函数(() => options)而非字面量对象。从 createBaseQuery.ts 的注释可以确认这是刻意设计:options 被包在函数里后,内部嵌入的 signal 表达式(如 this.postIdSignal())才能被 Angular 的响应式系统追踪,signal 变化时默认选项和 observer 才会自动重算。
  2. queryFn 被整体省略后,类型系统依然完整injectQuery 的泛型参数(Array<Post> / Post)直接决定了返回值信号的类型,无需本地声明返回 Promise 的函数;query-options.ts 中的 UnusedSkipTokenOptionsDefinedInitialDataOptions 等类型正是把 queryFn 声明为可选字段的依据,见 query-options.ts

源码层面:默认 queryFn 是如何生效的

第一步:createBaseQuery 用 computed 应用全局默认值

injectQueryinjectInfiniteQuery 共享同一个底层工厂 createBaseQuery。其中 defaultedOptionsSignal 是一个 computed,每次求值都会调用 queryClient.defaultQueryOptions(optionsFn()),把组件传入的 options 与全局默认值合并:

const defaultedOptionsSignal = computed(() => {
  const defaultedOptions = queryClient.defaultQueryOptions(optionsFn())
  defaultedOptions._optimisticResults = isRestoring() ? 'isRestoring' : 'optimistic'
  return defaultedOptions
})

create-base-query.ts。由于它是 computed,当 optionsFn() 内引用的 signal(例如 postIdSignal)变化时,合并结果会重新计算,enabledqueryKey 等也随之更新。

第二步:QueryClient.defaultQueryOptions 的合并顺序

合并逻辑在 QueryClient.defaultQueryOptions 中,核心是三段展开:

const defaultedOptions = {
  ...this.#defaultOptions.queries,   // ① 全局默认值(含我们的 defaultQueryFn)
  ...this.getQueryDefaults(options.queryKey), // ② 按 key 前缀匹配的 setQueryDefaults
  ...options,                        // ③ 本次调用显式传入的 options
  _defaulted: true,
}

从源码结构看,优先级由低到高是:

  1. defaultOptions.queries(构造 QueryClient 时传入,全局生效);
  2. setQueryDefaults(queryKey, options) 注册的按 key 前缀匹配的默认值getQueryDefaults 通过 partialMatchKey 做前缀匹配,见 queryClient.ts
  3. 每次 injectQuery 传入的 options,可以覆盖默认 queryFn 或任何其它字段。

这意味着:全局注册 defaultQueryFn 之后,某个特殊查询仍可在 options 里写自己的 queryFn 覆盖它;也可以只对 /posts/* 这一族 key 用 setQueryDefaults 提供局部默认值,而不影响其它 key。

方法还会补全若干派生默认值(如 refetchOnReconnectthrowOnErrornetworkMode),并对 skipTokenenabled = false 的转换,见 queryClient.ts。带 _defaulted 标记的对象会被直接短路返回,避免重复合并。

第三步:Query 执行时如何找到 queryFn

真正发起请求前,Query 会在 this.options.queryFn 缺失时,从已订阅的 observer 中找出携带 queryFn 的那个作为兜底(defaultOptions.queries.queryFn 正是在合并阶段被放进了 observer options):

if (!this.options.queryFn) {
  const observer = this.observers.find((x) => x.options.queryFn)

query.ts。随后通过 ensureQueryFn(this.options, fetchOptions) 解析出最终函数并以其构造 QueryFunctionContext 执行(query.ts)。这条链路解释了为什么「组件里不写 queryFn」仍然能发起请求:默认值在 observer 合并期就已经挂到 options 上,执行期只是读取。

默认值、queryOptions 与类型安全

如果你偏好把查询配置抽到组件外部复用,可以用同包的 queryOptions 帮助函数(query-options.ts)。它本身只是恒等函数,价值在于三个重载为 queryKey 打上 queryFn 的数据类型标签:

const { queryKey } = queryOptions({
  queryKey: ['key'],
  queryFn: () => Promise.resolve(5),
  //  ^? Promise<number>
})

打上标签后,queryClient.getQueryData(queryKey) 的返回值类型就是 number | undefined,而不是 unknown。把 queryOptions 与全局 defaultQueryFn 结合时,抽出的配置可以只含 queryKey,数据获取细节全部交给默认函数,同时保持端到端的类型推断。

实践建议与边界

结合仓库实现,给出几条落地时的注意点:

  • 让 key 成为「请求路径」的前提是请求层可控。示例中 queryKey[0] 直接拼 URL,适合 REST 且路径结构稳定的 API;对 GraphQL 或非路径型 key,默认函数应按 key 结构分派(从 queryKey 首元素判断资源类型)再选择对应请求逻辑。
  • 默认函数里做集中式错误/响应归一化。既然 queryFn 是全局单点,axios 拦截器式的解包(return data)、错误映射都适合放在这里,各组件无需感知。
  • 局部覆盖优于复制粘贴:个别查询的取数逻辑确实不同时,在该次 injectQuery 的 options 中显式写 queryFn 即可(合并顺序的第 ③ 层),不要为此拆出第二个 QueryClientprovideQueryClient 的文档注释也说明它更适用于应用局部或单元测试场景,见 providers.ts
  • 响应式依赖依赖 options 函数。默认 queryFn 的注册是「应用级静态配置」,而 enabledqueryKey 这类与 signal 相关的字段必须在 options 函数内引用 signal(如示例的 this.postIdSignal() > 0),才会随信号更新——这是 createBaseQuery.ts 注释中强调的行为。
  • 适用前提defaultOptions.queries.queryFn 由 query-core 提供,Angular 侧经由 @tanstack/angular-query-experimentalprovideTanStackQuery 注入客户端后自动生效;版本行为以本仓库当前 query-core 源码Angular 适配器源码 为准。

小结

默认 queryFn 模式把「如何请求」从 N 个组件收敛到 1 处 QueryClient 配置:defaultQueryFn 接收 queryKey 动态构造请求,provideTanStackQuery(queryClient) 完成应用级注入,injectQuery(() => ({ queryKey: [...] })) 则让组件代码退化为「声明 key + 泛型类型」。源码链路上,computed 包裹的 defaultQueryOptions 合并(全局默认 → 按 key 前缀默认 → 调用方 options)与 Query 执行期的 queryFn 兜底查找,共同保证了这套省略写法在响应式更新下的正确性。仓库中的 Angular 示例应用bootstrapApplication + appConfig 提供 provideTanStackQuery)可直接作为搭建起点。

登录后查看全文
热门项目推荐
相关项目推荐