TanStack Query Angular 默认 queryFn 实战:用 defaultOptions 为 injectQuery 省去逐条声明
本篇技术指南基于 TanStack Query(Angular 适配器 @tanstack/angular-query-experimental)的 default-query-function 指南,讲解如何通过 QueryClient 的 defaultOptions.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还可以放staleTime、retry等(本文聚焦queryFn);QueryClient构造器会把传入的defaultOptions保存下来(config.defaultOptions || {}),后续所有defaultQueryOptions(...)调用都从这里读取,见 QueryClient 构造函数。
把客户端接入 Angular 应用
bootstrapApplication(MyAppComponent, {
providers: [provideTanStackQuery(queryClient)],
})
provideTanStackQuery 内部通过 provideQueryClient 把 QueryClient 注册为可注入的单例,并在 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()}`],
}))
// ...
}
两个细节值得注意:
injectQuery接收一个 options 函数(() => options)而非字面量对象。从 createBaseQuery.ts 的注释可以确认这是刻意设计:options 被包在函数里后,内部嵌入的 signal 表达式(如this.postIdSignal())才能被 Angular 的响应式系统追踪,signal 变化时默认选项和 observer 才会自动重算。queryFn被整体省略后,类型系统依然完整。injectQuery的泛型参数(Array<Post>/Post)直接决定了返回值信号的类型,无需本地声明返回Promise的函数;query-options.ts中的UnusedSkipTokenOptions与DefinedInitialDataOptions等类型正是把queryFn声明为可选字段的依据,见 query-options.ts。
源码层面:默认 queryFn 是如何生效的
第一步:createBaseQuery 用 computed 应用全局默认值
injectQuery 与 injectInfiniteQuery 共享同一个底层工厂 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)变化时,合并结果会重新计算,enabled、queryKey 等也随之更新。
第二步:QueryClient.defaultQueryOptions 的合并顺序
合并逻辑在 QueryClient.defaultQueryOptions 中,核心是三段展开:
const defaultedOptions = {
...this.#defaultOptions.queries, // ① 全局默认值(含我们的 defaultQueryFn)
...this.getQueryDefaults(options.queryKey), // ② 按 key 前缀匹配的 setQueryDefaults
...options, // ③ 本次调用显式传入的 options
_defaulted: true,
}
从源码结构看,优先级由低到高是:
defaultOptions.queries(构造QueryClient时传入,全局生效);setQueryDefaults(queryKey, options)注册的按 key 前缀匹配的默认值,getQueryDefaults通过partialMatchKey做前缀匹配,见 queryClient.ts;- 每次
injectQuery传入的 options,可以覆盖默认queryFn或任何其它字段。
这意味着:全局注册 defaultQueryFn 之后,某个特殊查询仍可在 options 里写自己的 queryFn 覆盖它;也可以只对 /posts/* 这一族 key 用 setQueryDefaults 提供局部默认值,而不影响其它 key。
方法还会补全若干派生默认值(如 refetchOnReconnect、throwOnError、networkMode),并对 skipToken 做 enabled = 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即可(合并顺序的第 ③ 层),不要为此拆出第二个QueryClient;provideQueryClient的文档注释也说明它更适用于应用局部或单元测试场景,见 providers.ts。 - 响应式依赖依赖 options 函数。默认
queryFn的注册是「应用级静态配置」,而enabled、queryKey这类与 signal 相关的字段必须在 options 函数内引用 signal(如示例的this.postIdSignal() > 0),才会随信号更新——这是 createBaseQuery.ts 注释中强调的行为。 - 适用前提:
defaultOptions.queries.queryFn由 query-core 提供,Angular 侧经由@tanstack/angular-query-experimental的provideTanStackQuery注入客户端后自动生效;版本行为以本仓库当前 query-core 源码 与 Angular 适配器源码 为准。
小结
默认 queryFn 模式把「如何请求」从 N 个组件收敛到 1 处 QueryClient 配置:defaultQueryFn 接收 queryKey 动态构造请求,provideTanStackQuery(queryClient) 完成应用级注入,injectQuery(() => ({ queryKey: [...] })) 则让组件代码退化为「声明 key + 泛型类型」。源码链路上,computed 包裹的 defaultQueryOptions 合并(全局默认 → 按 key 前缀默认 → 调用方 options)与 Query 执行期的 queryFn 兜底查找,共同保证了这套省略写法在响应式更新下的正确性。仓库中的 Angular 示例应用(bootstrapApplication + appConfig 提供 provideTanStackQuery)可直接作为搭建起点。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00