深入解析 Angular Query 的 QueryFeature:基于 provideTanStackQuery 的可组合依赖注入特性架构
QueryFeature 是 TanStack Query 在 Angular Query(@tanstack/angular-query-experimental)中用于扩展 provideTanStackQuery 的核心类型契约:它把"某组额外的 Angular Provider"与一个可辨识的特性名绑定在一起,使依赖注入层面可以像搭积木一样组合出开发者工具、持久化等能力。本文将以接口与源码为主线,讲透 ɵkind / ɵproviders 两个字段的含义、queryFeature 工厂函数的作用,并结合仓库内 DevTools 与持久化两个真实 Feature 的实现与配置选项,给出可复制的实战配置。
一、为什么需要 QueryFeature:Angular Query 的可插拔 Provider 设计
在 Angular 中使用 TanStack Query,最常规的入口是 provideTanStackQuery。它负责两件事:一是把 QueryClient 通过依赖注入提供给整个应用;二是按需携带一组"额外能力"。如果你只想要核心查询能力,只需传入 new QueryClient();如果你还希望有调试面板或离线持久化,就要额外传入"特性"。
而这些特性在类型层面被统一抽象为一个接口——QueryFeature<TFeatureKind>,其定义位于 providers.ts:
export interface QueryFeature<TFeatureKind extends QueryFeatureKind> {
ɵkind: TFeatureKind
ɵproviders: Array<Provider>
}
一句话理解这个设计:一个 QueryFeature 就是"一个名字 + 一组 Provider"。ɵkind 是仅供类型区分与内部标识使用的特性名,ɵproviders 才是真正会被 Angular 注入系统消费的 Provider 数组。provideTanStackQuery 拿到所有 Feature 后,只需要把它们的 ɵproviders 逐一展开合并到根注入器中即可,因此整个扩展机制不依赖任何框架黑魔法,纯粹是标准的 Angular DI 组合。
说明:字段名中的
ɵ前缀沿用了 Angular 内部符号的命名习惯,意在强调"这是面向框架集成的私有约定、业务代码一般不应直接依赖"。在本文对应的 API 参考文档 QueryFeature.md 中同样可以看到这两个字段以原样列出。
二、QueryFeature 接口逐字段剖析
官方参考文档 QueryFeature.md 将该接口描述为 "Helper type to represent a Query feature"(用于表示一个 Query 特性的辅助类型),并给出两个可读属性:
2.1 ɵkind
ɵkind: TFeatureKind
- 类型:泛型参数
TFeatureKind,其约束为TFeatureKind extends QueryFeatureKind。 - 取值来源:源码中特性名集合是一个常量元组:
见 providers.ts。因此当前合法的const queryFeatures = ['Devtools', 'PersistQueryClient'] as const type QueryFeatureKind = (typeof queryFeatures)[number]QueryFeatureKind即'Devtools' | 'PersistQueryClient'两种字面量。 - 作用:作为特征的"类型级标识",让
QueryFeature<'Devtools'>与QueryFeature<'PersistQueryClient'>在 TypeScript 类型系统中彼此区分,进而派生出两个语义明确的类型别名(详见第四节)。编译后它只是对象上的一个普通字符串字段,不参与实际的 Provider 解析逻辑——真正生效的是ɵproviders。
2.2 ɵproviders
ɵproviders: Provider[];
- 类型:
Array<Provider>,即标准的 Angular DI Provider 数组(可以是类 Provider、值 Provider、工厂 Provider、ENVIRONMENT_INITIALIZER等多 token 集合)。 - 作用:这是 Feature 的"载荷",携带启用该能力所需的全部依赖注入配置。
provideTanStackQuery正是把所有 Feature 的ɵproviders拍平后注入环境:
export function provideTanStackQuery(
queryClient: QueryClient | InjectionToken<QueryClient>,
...features: Array<QueryFeatures>
): Array<Provider> {
return [
provideQueryClient(queryClient),
features.map((feature) => feature.ɵproviders),
]
}
见 providers.ts。注意 Array<Provider> 是可以嵌套的(features.map(...) 会产生二维数组),Angular 的 DI 系统会递归展开,因此写法上直接放行即可。
三、工厂函数 queryFeature:创建特性的标准姿势
既然 Feature 只是一个普通的对象字面量结构,理论上可以手写 { ɵkind: 'Devtools', ɵproviders: [...] }。但仓库提供了更受控的工厂函数 queryFeature(见 providers.ts):
export function queryFeature<TFeatureKind extends QueryFeatureKind>(
kind: TFeatureKind,
providers: Array<Provider>,
): QueryFeature<TFeatureKind> {
return { ɵkind: kind, ɵproviders: providers }
}
- 两个入参分别是特性名与 Provider 数组,返回值正是
QueryFeature<TFeatureKind>,类型与文档签名完全一致(见 queryFeature.md); - 该函数通过
@tanstack/angular-query-experimental的公共入口导出(见 index.ts),并被内部 Feature 实现复用; - 它可以被看作一种 trait/能力包的规范封装:想为 Angular Query 增加一类可插拔能力,就按此约定返回一个
QueryFeature。
需要强调的是,queryFeature 并不自行向注入器注册任何东西——它只负责"装箱",真正"开箱使用"发生在 provideTanStackQuery 或等价调用中。
四、两大内建 Feature:DevtoolsFeature 与 PersistQueryClientFeature
4.1 特性类型全家桶
仓库把"哪些 Feature 可以传给 provideTanStackQuery"统一成联合类型 QueryFeatures(见 providers.ts):
export type DevtoolsFeature = QueryFeature<'Devtools'>
export type PersistQueryClientFeature = QueryFeature<'PersistQueryClient'>
export type QueryFeatures = DevtoolsFeature | PersistQueryClientFeature
DevtoolsFeature是withDevtools()的返回值类型,对应 DevTools 面板能力;PersistQueryClientFeature是持久化特性的返回值类型,定义在独立包@tanstack/angular-query-persist-client的 with-persist-query-client.ts 中。
对应的类型别名参考文档:DevtoolsFeature.md、PersistQueryClientFeature.md、QueryFeatures.md。
4.2 Devtools Feature 的源码级实现
withDevtools 的实现位于 with-devtools.ts,其骨架正是对 queryFeature('Devtools', [...]) 的调用。它向 ɵproviders 中注入两样东西:
- 一个内部
InjectionToken(DEVTOOLS_OPTIONS_SIGNAL),用computed(() => withDevtoolsFn?.(...deps) ?? {})把配置函数转成响应式信号; - 一个
ENVIRONMENT_INITIALIZER(多值),在环境初始化阶段执行懒加载逻辑:- 仅当运行在浏览器平台(
isPlatformBrowser(PLATFORM_ID))时才真正加载,服务端渲染时直接跳过; - 通过
import('@tanstack/query-devtools')动态引入 DevTools,从而不会进入首屏主包; - 加载开关
shouldLoadToolsSignal默认取isDevMode(),即开发模式自动加载、生产模式自动跳过。
- 仅当运行在浏览器平台(
同时,包清单 package.json 中为 @tanstack/angular-query-experimental/devtools 提供了"开发用真实实现 + 生产用 stub"的双重导出,进一步保证生产包体最小化。
DevtoolsOptions(定义于 types.ts)可配置项如下:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
initialIsOpen |
boolean |
false |
是否默认展开 DevTools 面板 |
buttonPosition |
'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'relative' |
'bottom-right' |
TanStack 开合按钮的位置 |
position |
'top' | 'bottom' | 'left' | 'right' |
'bottom' |
DevTools 面板的停靠方位 |
client |
QueryClient |
注入的全局实例 | 自定义要调试的 QueryClient |
errorTypes |
Array<DevtoolsErrorType> |
— | 自定义错误类型,便于在面板中区分展示 |
styleNonce |
string |
— | 配合 CSP(内容安全策略)的内联样式 nonce |
shadowDOMTarget |
ShadowRoot |
— | 将面板样式挂载到指定 Shadow DOM 根节点 |
hideDisabledQueries |
boolean |
— | 是否隐藏被禁用(disabled)的查询 |
theme |
'light' | 'dark' | 'system' |
'system' |
面板主题 |
loadDevtools |
'auto' | boolean |
'auto' |
加载策略:auto=开发模式懒加载;true=任何环境都加载;false=任何环境都不加载 |
其中 loadDevtools: 'auto' 是默认且推荐的做法;测试等特殊场景可通过传 true/false 覆盖默认行为(详见文档注释),也可以通过配置回调返回一个响应式 Signal,实现"按快捷键/按条件动态开关 DevTools"。
4.3 Persist Query Client Feature:跨包复用同一机制的范例
QueryFeature 抽象的价值在跨包复用上体现得最充分:持久化特性不在 angular-query-experimental 内实现,而是由独立包 @tanstack/angular-query-persist-client 提供。其 withPersistQueryClient 返回类型直接声明为 PersistQueryClientFeature,内部同样构造一组 Provider:
const providers = [
provideIsRestoring(isRestoring.asReadonly()),
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useValue: () => {
if (!isPlatformBrowser(inject(PLATFORM_ID))) return
// ...
persistQueryClientRestore(options)
.then(...)
.finally(() => { isRestoring.set(false) })
},
},
]
可以看到它复用了 queryFeature 类型(从 @tanstack/angular-query-experimental 导入 PersistQueryClientFeature),并以 provideIsRestoring + ENVIRONMENT_INITIALIZER 的形式进入注入器。也就是说:同一套 QueryFeature 类型契约,让不同包维护的扩展都能被 provideTanStackQuery 无缝接纳。
五、实战:在应用入口组合 Feature
5.1 Standalone 引导(最常用)
import {
provideTanStackQuery,
withDevtools,
QueryClient,
} from '@tanstack/angular-query-experimental'
import { bootstrapApplication } from '@angular/platform-browser'
bootstrapApplication(AppComponent, {
providers: [
provideTanStackQuery(new QueryClient(), withDevtools()),
],
})
withDevtools() 返回 DevtoolsFeature,其 Provider 会被展开到应用根注入器;运行时若处于开发模式,动态加载的 DevTools 会渲染在 <body> 中。该示例同时出现在 provideTanStackQuery 参考文档 与 quick-start 中。
5.2 NgModule 方式
import {
provideTanStackQuery,
QueryClient,
} from '@tanstack/angular-query-experimental'
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [provideTanStackQuery(new QueryClient())],
bootstrap: [AppComponent],
})
export class AppModule {}
5.3 通过 InjectionToken 延迟提供 QueryClient(懒加载优化)
如果希望 TanStack Query 不出现在主包、只在懒加载路由中使用,可以传 InjectionToken<QueryClient>:
export const MY_QUERY_CLIENT = new InjectionToken('', {
factory: () => new QueryClient(),
})
// 在懒加载路由或懒加载组件的 providers 数组中:
providers: [provideTanStackQuery(MY_QUERY_CLIENT)]
其底层依赖 provideQueryClient:无论传入的是实例还是 token,都会在 useFactory 中解析出客户端,并调用 client.mount(),同时借助 DestroyRef 在注入器销毁时 unmount(),确保生命周期闭环。文档注释也提醒:这属于较小的优化,大多数应用仍建议在应用根配置中直接提供 QueryClient。
5.4 同时启用持久化与 DevTools
import { provideTanStackQuery } from '@tanstack/angular-query-experimental'
import { withPersistQueryClient } from '@tanstack/angular-query-persist-client'
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
const localStoragePersister = createAsyncStoragePersister({
storage: window.localStorage,
})
export const appConfig: ApplicationConfig = {
providers: [
provideTanStackQuery(
new QueryClient(),
withDevtools(), // QueryFeature<'Devtools'>
withPersistQueryClient({
persistOptions: { persister: localStoragePersister },
onSuccess: () => console.log('Restoration completed successfully.'),
}), // QueryFeature<'PersistQueryClient'>
),
],
}
两个 Feature 一起作为 rest 参数传入,provideTanStackQuery 会分别展开各自 ɵproviders,互不干扰。withPersistQueryClient 的 persistOptions 还支持底层 query-persist-client-core 提供的 maxAge、buster、dehydrateOptions、hydrateOptions 等字段,用于控制缓存有效期、失效标记与序列化策略。
六、进阶:Feature 机制的边界与定位
从源码结构可以推断,这套 QueryFeature 抽象借鉴了 Angular 生态中常见的 with* 函数式 Provider 组合风格(类似 provideRouter 搭配各类 withXxx 路由特性)。它的价值主要体现在:
- 类型安全:
QueryFeatures联合类型限制了provideTanStackQuery只能接收已登记的特性,拼错名字会在编译期报错; - 按需组合与按包分发:DevTools 留在主包中实现,持久化放到独立包实现,双方只需遵守
QueryFeature契约; - 与 Angular DI 原生融合:Feature 内部既可用
ENVIRONMENT_INITIALIZER做环境初始化钩子,也可用provideIsRestoring暴露跨模块信号,没有引入任何非 Angular 的注册机制。
若需要低于 provideTanStackQuery 粒度的控制(例如把 DevTools 渲染进你自己的壳应用/内嵌面板),仓库还提供了 injectDevtoolsPanel 等更底层的 API(见 inject-devtools-panel.ts),DevTools 文档见 devtools.md;测试场景如何注入隔离的 QueryClient 可参考 testing.md。
七、快速索引
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 StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python08
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00