Angular Query 的 InjectInfiniteQueryOptions 深度解析:用 injector 掌控无限查询的依赖注入上下文
在 TanStack Query 的 Angular 适配层(
@tanstack/angular-query-experimental)中,injectInfiniteQuery用于以声明式方式订阅可无限追加("load more" / "无限滚动")的数据源。而本文主角InjectInfiniteQueryOptions正是该函数的第二个可选参数的类型契约——它通过唯一的injector属性,决定无限查询应当从哪个Injector中创建。读懂它,你就掌握了在非标准注入上下文(如单元测试、手动构建的服务实例)中正确创建无限查询的关键。
概述:这个接口到底长什么样
InjectInfiniteQueryOptions 是 injectInfiniteQuery 的第二参数 options 的类型,定义于源码 inject-infinite-query.ts,接口本身极其精简,只有一个可选属性:
export interface InjectInfiniteQueryOptions {
/**
* The `Injector` in which to create the infinite query.
*
* If this is not provided, the current injection context will be used instead (via `inject`).
*/
injector?: Injector
}
也就是说,它的职责非常单一:允许调用方显式指定"在哪里创建无限查询"。与之配套的入口签名是:
function injectInfiniteQuery<...>(
injectInfiniteQueryFn: () => CreateInfiniteQueryOptions<...>,
options?: InjectInfiniteQueryOptions, // <- 本文主角
): CreateInfiniteQueryResult<...>
函数级文档可对照 injectInfiniteQuery(函数参考) 阅读;查询选项本体 CreateInfiniteQueryOptions 的字段则见 CreateInfiniteQueryOptions 接口。
属性详解:injector?
optional injector: Injector;
Injector 是 Angular 依赖注入体系的根类型(@angular/core 导出)。该属性语义如下:
- 作用:指定"创建这个无限查询"所在的注入器。
- 默认行为:如果不传该属性,则使用当前注入上下文(
inject)获得当前所在的Injector。
典型场景下(在 Angular 组件、指令、管道、服务构造函数等注入上下文内调用),省略 injector 完全没问题,Angular Query 会自动拾取当前注入器,并从中解析出后续所需的依赖。
为什么需要 injector:看看底层实现
要理解这个选项的价值,需要查看 inject-infinite-query.ts 中函数体的完整实现:
export function injectInfiniteQuery(
injectInfiniteQueryFn: () => CreateInfiniteQueryOptions,
options?: InjectInfiniteQueryOptions,
) {
// 1) 未显式提供 injector 时,强制要求处于注入上下文
!options?.injector && assertInInjectionContext(injectInfiniteQuery)
// 2) 有 injector 用 injector,没有则从当前注入上下文解析
const injector = options?.injector ?? inject(Injector)
// 3) 在目标注入器内执行真正的查询创建逻辑
return runInInjectionContext(injector, () =>
createBaseQuery(
injectInfiniteQueryFn,
InfiniteQueryObserver as typeof QueryObserver,
),
)
}
第一步:注入上下文守卫
!options?.injector && assertInInjectionContext(injectInfiniteQuery) 是前置校验:
- 若你没有传
injector,但又不在组件/指令/服务构造器等注入上下文里,assertInInjectionContext会抛出 NG0203 错误("inject()必须在注入上下文中调用"),并且因为传入的是injectInfiniteQuery函数本身,报错信息会指明违规调用来自injectInfiniteQuery,便于定位。 - 反过来,只要你显式传了
injector,这一守卫就会被短路跳过——这正是测试中"在注入上下文之外也能使用"的实现基础。
第二步与第三步:选定注入器并执行
injector = options?.injector ?? inject(Injector) 采用"显式优先"策略;随后用 Angular 提供的 runInInjectionContext(injector, fn) 把 createBaseQuery(...) 放进指定的注入器执行。createBaseQuery 是 injectQuery 与 injectInfiniteQuery 共享的底座,其内部会以 inject(...) 的方式解析一串运行时依赖(见 create-base-query.ts):
NgZone:负责把查询状态更新重新调度回 Angular zone;PENDING_TASKS:配合fetchStatus维持 Angular 的 pending 任务计数(用于 SSR/水合等待);QueryClient:查询客户端,整个查询缓存与去重机制的核心;injectIsRestoring():判断是否处于持久化恢复状态。
因此,传入不同的 injector,实际上就是换了一套 QueryClient / NgZone 等依赖来源。在多 QueryClient 分层(例如需要局部环境覆盖全局默认客户端)的架构中,这一选项提供了精确的"从哪个环境创建查询"的能力。
什么时候该用 injector(实战场景)
场景一:在注入上下文之外创建无限查询
凡是"没有现成注入上下文、但持有某个注入器实例"的地方,都需要手动传 injector。例如把查询创建逻辑封装进一个由测试框架初始化的 Injector,或配合 Injector.create() 手动搭建迷你环境时。
场景二:单元测试中直接驱动查询
仓库自带测试 inject-infinite-query.test.ts 就专门覆盖了这两类行为:
describe('injection context', () => {
it('should throw NG0203 with descriptive error outside injection context', () => {
expect(() => {
injectInfiniteQuery(() => ({ /* ... */ }))
}).toThrow(/NG0203(.*?)injectInfiniteQuery/)
})
it('should be usable outside injection context when passing an injector', () => {
const query = injectInfiniteQuery(
() => ({
queryKey: key,
queryFn: ({ pageParam }) =>
sleep(0).then(() => 'data on page ' + pageParam),
initialPageParam: 0,
getNextPageParam: () => 12,
}),
{
injector: TestBed.inject(Injector), // <- 显式指定注入器
},
)
expect(query.status()).toBe('pending')
})
})
对照组清晰说明了 injector 的边界语义:默认不传会因 NG0203 失败;传入 TestBed.inject(Injector) 后,即便代码并不运行在任何组件上下文中,无限查询也能正常创建并进入 pending 状态。
不传 injector 的常规用法与结果信号
绝大多数业务代码都可以完全忽略第二个参数。下面是在组件中声明式创建无限查询的常规形态(来源参见 Angular Infinite Queries 指南):
import { Component, computed, inject } from '@angular/core'
import { injectInfiniteQuery } from '@tanstack/angular-query-experimental'
import { lastValueFrom } from 'rxjs'
import { ProjectsService } from './projects-service'
@Component({
selector: 'example',
templateUrl: './example.component.html',
})
export class Example {
projectsService = inject(ProjectsService)
query = injectInfiniteQuery(() => ({
queryKey: ['projects'],
queryFn: async ({ pageParam }) => {
return lastValueFrom(this.projectsService.getProjects(pageParam))
},
initialPageParam: 0,
getPreviousPageParam: (firstPage) => firstPage.previousId ?? undefined,
getNextPageParam: (lastPage) => lastPage.nextId ?? undefined,
maxPages: 3,
}))
}
由于该调用处于组件构造的注入上下文中,injector 会被自动推导为组件所在环境,无需显式传入。返回的查询结果(由 signalProxy 包装)包含一组信号式成员,供模板和逻辑消费,例如 status()、data()、error()、isFetchingNextPage()、hasNextPage(),以及 fetchNextPage() / fetchPreviousPage() 方法:
@if (query.isPending()) {
<p>Loading...</p>
} @else if (query.isError()) {
<span>Error: {{ query?.error().message }}</span>
} @else {
@for (page of query?.data().pages; track $index) {
@for (project of page.data; track project.id) {
<p>{{ project.name }} {{ project.id }}</p>
}
}
}
无限查询选项的核心组成
虽然 InjectInfiniteQueryOptions 只管 injector,无限查询本身的行为选项全部位于传给第一参数的 CreateInfiniteQueryOptions 中,二者不应混淆。核心行为字段如下(更完整的字段见 CreateInfiniteQueryOptions 接口):
| 字段 | 作用 |
|---|---|
queryKey |
唯一标识该查询,跨组件共享缓存去重的依据 |
queryFn({ pageParam }) |
按当前页参数拉取一页数据 |
initialPageParam |
第一页的起始页参数,必填 |
getNextPageParam |
根据最后一页推导下一页参数;返回 undefined 表示没有更多页 |
getPreviousPageParam |
向前翻页时推导上一页参数 |
maxPages |
最多保留的页数上限(配合大量追加时裁剪旧页) |
initialData |
同步初始数据 |
深入返回类型与重载
injectInfiniteQuery 针对 initialData 的不同形态提供了三组重载,而 InjectInfiniteQueryOptions 在每一组中都作为第二个参数参与签名(见 inject-infinite-query.ts):
| 重载输入(第一参数返回的类型) | 返回类型 | 语义 |
|---|---|---|
DefinedInitialDataInfiniteOptions<...> |
DefinedCreateInfiniteQueryResult<TData, TError> |
提供了非空 initialData,data 信号被收窄为非空 |
UndefinedInitialDataInfiniteOptions<...> |
CreateInfiniteQueryResult<TData, TError> |
initialData 可能为 undefined,data 可为空 |
CreateInfiniteQueryOptions<...> |
CreateInfiniteQueryResult<TData, TError> |
通用兜底签名 |
这些"初始数据有界"类型由 infinite-query-options.ts 提供,它同时导出了 infiniteQueryOptions() 工厂函数,可以把选项以类型安全的方式与 queryKey 的类型标签绑定后复用。泛型参数方面,常见的类型参数默认值包括:TError = DefaultError、TData = InfiniteData<TQueryFnData>、TQueryKey extends QueryKey、TPageParam = unknown——即默认情况下 data() 是一个 InfiniteData(内含 pages 与 pageParams)。
测试如何验证整体行为
除了上述注入上下文专项测试,inject-infinite-query.test.ts 还完整验证了无限查询的核心生命周期:
- 成功路径:用假定时器推进后,状态从
pending变为success,pages依次累积data on page 0、data on page 12,证明getNextPageParam与fetchNextPage()的追加语义正确(第 30-67 行); - 失败路径:
retry: false时错误被吸收进状态机,status变为error、isError()为true、failureCount()递增为1(第 69-105 行)。
类型层面的编译期约束则由 inject-infinite-query.test-d.ts 与 infinite-query-options.test-d.ts 保障。
汇总:一张图看懂接口关系
injectInfiniteQuery( optionsFn, options?: InjectInfiniteQueryOptions )
│
└── injector?: Injector
├── 省略 => 使用当前注入上下文(inject(Injector))
└── 传入 => 在 runInInjectionContext(injector, ...) 中
执行 createBaseQuery(fn, InfiniteQueryObserver)
│
└── inject(NgZone / PENDING_TASKS /
QueryClient / isRestoring)
延伸阅读
- InjectInfiniteQueryOptions 官方接口文档
- injectInfiniteQuery 函数参考(含三组重载签名)
- CreateInfiniteQueryOptions 接口参考
- Angular Infinite Queries 实战指南
- 接口定义与实现源码
- 共享底座 createBaseQuery 源码
- 类型安全选项工厂 infinite-query-options.ts
- 行为测试 inject-infinite-query.test.ts
- 配套可运行示例:infinite-query-with-max-pages
一句话总结:InjectInfiniteQueryOptions 是 injectInfiniteQuery 的第二参数,其唯一属性 injector 决定无限查询的依赖注入来源;省略时自动采用当前注入上下文并受 NG0203 守卫保护,显式传入时则允许在测试或非标准上下文中自由创建查询——是理解 Angular Query 响应式注入机制与调试注入错误的关键拼图。
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 StartedRust0627
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