ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Angular Query 快速上手:基于 Signals 的异步数据获取、缓存与服务端状态管理

2026/9/10 12:45:50 拓冰建站 浏览量
Angular Query 快速上手:基于 Signals 的异步数据获取、缓存与服务端状态管理 Angular Query 快速上手基于 Signals 的异步数据获取、缓存与服务端状态管理【免费下载链接】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/queryAngular Query 是 TanStack Query 在 Angular 生态中的官方适配层本仓库中对应tanstack/angular-query-experimental包。它以 Angular Signals 为核心抽象为 Angular 16 应用提供传输层无关的数据获取REST、GraphQL、Promise 均可、自动缓存与重新获取stale-while-revalidate、窗口聚焦刷新、轮询、并行/依赖查询、变更Mutation以及无限滚动等能力。读完本文你将掌握从零初始化 TanStack Query、通过injectQuery在组件与服务中以响应式方式声明数据依赖并理解其基于computed/effect/QueryObserver的底层实现原理。一、包定位与实验性阶段说明本仓库中的tanstack/angular-query-experimental是 TanStack Query 的 Angular 适配器其 README.md 开篇即明确声明该库当前处于实验性阶段这意味着在 minor 和 patch 版本中都可能发生破坏性变更。如果在生产环境使用请将版本锁定到 patch 级版本以避免意外破坏。这一点在 package.json 中也有印证当前版本为5.102.8peerDependencies 要求angular/common 16.0.0与angular/core 16.0.0而仓库内部开发依赖已升级到 Angular 20类型测试则覆盖 TypeScript 5.6 至 7.0 多个版本见test:types:*脚本说明该包对 Angular 版本有较宽的前向兼容范围。特性总览Quick FeaturesREADME 归纳了该包的核心能力这也是理解整篇文章内容地图的关键传输/协议/后端无关的数据获取REST、GraphQL、Promise 等一切异步来源皆可自动缓存 重新获取stale-while-revalidate、窗口聚焦刷新、轮询/实时更新并行查询与依赖查询Parallel Dependent Queries变更Mutations与响应式查询重新获取多层缓存 自动垃圾回收分页与基于游标的查询加载更多 / 无限滚动查询支持滚动位置恢复请求取消Request Cancellation专用开发者工具Devtools。二、安装README 提供了四种包管理器的一键安装方式任选其一$ npm i tanstack/angular-query-experimental$ pnpm add tanstack/angular-query-experimental$ yarn add tanstack/angular-query-experimental$ bun add tanstack/angular-query-experimental安装时需要注意两点Angular 版本前提该适配器要求 Angular 16 或更高版本与peerDependencies声明一致。可选依赖开发者工具面板依赖可选的tanstack/query-devtools包。在 with-devtools.ts 中如果动态加载失败会提示“Install tanstack/query-devtools or reinstall without --omitoptional”即使用pnpm install --omitoptional这类方式安装时可能不会带上该可选依赖。三、初始化provideTanStackQuery与QueryClientStandalone 应用推荐import { provideTanStackQuery } from tanstack/angular-query-experimental import { QueryClient } from tanstack/angular-query-experimental bootstrapApplication(AppComponent, { providers: [provideTanStackQuery(new QueryClient())], })NgModule 应用import { provideHttpClient } from angular/common/http import { provideTanStackQuery, QueryClient, } from tanstack/angular-query-experimental NgModule({ declarations: [AppComponent], imports: [BrowserModule], providers: [provideTanStackQuery(new QueryClient())], bootstrap: [AppComponent], })底层机制provideQueryClientprovideTanStackQuery内部委托给provideQueryClient见 providers.ts。该函数以useFactory形式将QueryClient注册到 DI 容器并做了两件关键的生命周期管理client.mount()立即挂载 client启动其内部订阅与调度inject(DestroyRef).onDestroy(() client.unmount())当所在注入器销毁时自动卸载防止内存泄漏。export function provideQueryClient( queryClient: QueryClient | InjectionTokenQueryClient, ): Provider { return { provide: QueryClient, useFactory: () { const client queryClient instanceof InjectionToken ? inject(queryClient) : queryClient // Unmount the query client on injector destroy inject(DestroyRef).onDestroy(() client.unmount()) client.mount() return client }, } }进阶用法一通过 InjectionToken 懒加载提供provideTanStackQuery/provideQueryClient都接受QueryClient实例或InjectionTokenQueryClient。源码注释providers.ts说明了这一高级优化场景在懒加载路由或懒加载组件的 providers 中传入 token可让 TanStack Query 不出现在主应用 bundle 中同时仍与主应用共享同一个QueryClient。export const MY_QUERY_CLIENT new InjectionToken(, { factory: () new QueryClient(), }) // 在懒加载路由或懒加载组件的 providers 数组中 providers: [provideTanStackQuery(MY_QUERY_CLIENT)]源码注释也提醒这是一个小规模优化对大多数应用而言直接在主应用配置中提供QueryClient是更可取的方案。进阶用法二Features 机制与withDevtoolsprovideTanStackQuery(queryClient, ...features)的第二个可变参数支持“功能特性”内部通过queryFeature工具函数收集providers.tsexport function provideTanStackQuery( queryClient: QueryClient | InjectionTokenQueryClient, ...features: ArrayQueryFeatures ): ArrayProvider { return [ provideQueryClient(queryClient), features.map((feature) feature.ɵproviders), ] }当前可用的特性类型为DevtoolsFeature与PersistQueryClientFeatureproviders.ts。其中 Devtools 通过withDevtools()启用默认仅在开发模式下加载isDevMode()生产构建中会被替换为无操作 stub——这一点与 package.json 中exports的development/default条件导出devtools 子路径在生产环境指向stub.mjs相互印证。import { provideTanStackQuery, withDevtools, QueryClient, } from tanstack/angular-query-experimental bootstrapApplication(AppComponent, { providers: [ provideTanStackQuery(new QueryClient(), withDevtools()), ], })在 with-devtools.ts 的实现中devtools 的加载还做了三重防护非浏览器平台SSR下直接noop、通过DEVTOOLS_PROVIDED内部 token 防止子注入器重复提供、动态import(tanstack/query-devtools)按需加载后挂载到body。兼容提示provideAngularQuery旧 APIprovideAngularQuery(queryClient)仍然导出但已被标记为deprecated其实现就是一行委托providers.tsexport function provideAngularQuery(queryClient: QueryClient): ArrayProvider { return provideTanStackQuery(queryClient) }新代码应统一使用provideTanStackQuery。四、注入查询injectQuery初始化完成后就可以在组件中注入查询了。README 给出的最小示例import { injectQuery } from tanstack/angular-query-experimental import { Component } from angular/core Component({...}) export class TodosComponent { info injectQuery(() ({ queryKey: [todos], queryFn: fetchTodoList })) }injectQuery接收一个返回 query options 的函数返回一个“信号代理对象”其字段data、isPending、isError、error、status、isFetching等都是 AngularSignal可直接在模板与computed/effect中使用。响应式选项让查询随 Signals 变化README 特别强调如果你需要动态更新查询选项请以 Signals 方式传递当更新的 query key 数据过期或不存在时查询会自动重新获取。Component({}) export class PostComponent { #postsService inject(PostsService) postId input.required({ transform: numberAttribute, }) postQuery injectQuery(() ({ queryKey: [post, this.postId()], queryFn: () { return lastValueFrom(this.#postsService.postById$(this.postId())) }, })) } Injectable({ providedIn: root, }) export class PostsService { #http inject(HttpClient) postById$ (postId: number) this.#http.getPost(https://jsonplaceholder.typicode.com/posts/${postId}) } export interface Post { id: number title: string body: string }这个示例演示了三种能力的组合Signal 输入input.required()定义的postId是 Angular 组件输入信号通过numberAttribute转换响应式 queryKeyqueryKey: [post, this.postId()]在函数体内读取信号postId变化时 query key 随之变化触发对新数据的获取旧 key 数据保留在缓存中query key 不变时则复用缓存不会重复请求RxJS 互操作HttpClient返回 Observable通过lastValueFrom转换为 Promise 作为queryFn的返回值——TanStack Query 天然接受 Promise 作为数据源。injectQuery的源码inject-query.ts显示其核心实现逻辑export function injectQuery( injectQueryFn: () CreateQueryOptions, options?: InjectQueryOptions, ) { !options?.injector assertInInjectionContext(injectQuery) return runInInjectionContext(options?.injector ?? inject(Injector), () createBaseQuery(injectQueryFn, QueryObserver), ) as unknown as CreateQueryResult }要点解读必须在注入上下文injection context中调用否则assertInInjectionContext会抛错可通过可选的options.injector参数显式指定在哪个Injector中创建查询便于在服务或脱离组件上下文的场景使用查询在runInInjectionContext内创建确保inject(QueryClient)等依赖注入正常工作所有泛型重载DefinedInitialDataOptions/UndefinedInitialDataOptions/CreateQueryOptions都是为了在“是否提供initialData”时给出更精确的返回值类型提供initialData时data字段类型不为undefined。响应式上下文类似 Angularcomputed的行为源码注释inject-query.ts明确说明传给injectQuery的函数会在响应式上下文中执行行为类似computed。例如下面的例子中当filter信号变为真值时查询自动启用并执行变为假值时查询自动禁用class ServiceOrComponent { filter signal() todosQuery injectQuery(() ({ queryKey: [todos, this.filter()], queryFn: () fetchTodos(this.filter()), // Signals can be combined with expressions enabled: !!this.filter(), })) }五、响应式实现原理createBaseQuery与signalProxyinjectQuery与injectInfiniteQuery共享同一个底层实现createBaseQuerycreate-base-query.ts其内部构造了一条完整的“信号管道”理解它有助于排查响应式失效问题。1. 默认选项计算defaultedOptionsSignalconst defaultedOptionsSignal computed(() { const defaultedOptions queryClient.defaultQueryOptions(optionsFn()) defaultedOptions._optimisticResults isRestoring() ? isRestoring : optimistic return defaultedOptions })用computed包裹选项函数使信号可以嵌入选项并保持响应式每次信号变化都会重新计算并与QueryClient的默认选项合并。2. Observer 单例observerSignalQueryObserver实例被闭包缓存只创建一次之后每次计算都返回同一实例instance || new Observer(...)选项变化通过observer.setOptions()传递。3. 乐观结果optimisticResultSignalconst optimisticResultSignal computed(() observerSignal().getOptimisticResult(defaultedOptionsSignal()), )在订阅建立前先用getOptimisticResult立即得到基于当前选项的“乐观”结果实现同步渲染首帧避免闪烁。4. 订阅与 NgZone 调度第二个effectcreate-base-query.ts完成了订阅的核心工作在ngZone.runOutsideAngular中订阅 observer避免每个通知都触发变更检测回调经notifyManager.batchCalls批处理再通过ngZone.run回到 Angular 变更检测Pending Tasks 跟踪fetchStatus fetching时调用pendingTasks.add()增加挂起任务变为idle时释放——这正是 Angular 应用加载指示器如ngx-loading-bar、应用启动等待能感知异步查询的原因错误抛出策略当state.isError !state.isFetching且满足shouldThrowError(observer.options.throwOnError, ...)时通过ngZone.onError.emit(state.error)抛出错误对应 React Query 中throwOnError的语义组件销毁时通过onCleanup释放 pending task 并退订避免泄漏。5. 结果对象signalProxy最终返回的对象由 signal-proxy.ts 中的signalProxy生成——一个Proxy其行为是对象的每个非函数字段被包装为Computed信号如result.data、result.status函数字段如refetch原样透传惰性创建首次访问某字段时才生成对应的 computed且内部缓存。export function signalProxyTInput extends Recordstring | symbol, any( inputSignal: SignalTInput, ) { const internalState {} as MapToSignalsTInput return new ProxyMapToSignalsTInput(internalState, { get(target, prop) { // 先查内部缓存 const computedField target[prop] if (computedField) return computedField // 函数字段直接返回 const targetField untracked(inputSignal)[prop] if (typeof targetField function) return targetField // 其余字段包装为 computed 并缓存 return (target[prop] computed(() inputSignal()[prop])) }, // ... }) }此外返回结果中的refetch被特殊包装调用前先observer.setOptions(defaultedOptionsSignal())确保以最新选项执行重新获取。六、更多注入 APIMutation、Infinite Query 与查询工具包的主入口 index.ts 完整导出以下注入 APIREADME 之外的这些能力均可直接使用API说明底层 ObserverinjectQuery单查询QueryObserverinjectInfiniteQuery无限滚动/加载更多查询InfiniteQueryObserverinjectMutation变更写操作MutationObserverinjectQueries并行执行多个查询experimental 子路径QueriesObserverinjectMutationState订阅全局 mutation 状态列表—injectIsFetching全局是否有查询正在获取—injectIsMutating全局是否有 mutation 进行中—injectIsRestoring是否正在恢复持久化缓存—injectQueryClient直接注入QueryClient实例—injectMutation写操作与查询不同mutation 不会自动执行必须显式调用mutate或mutateAsync。见 inject-mutation.tsclass ServiceOrComponent { mutation injectMutation(() ({ mutationFn: (variables: NewTodo) lastValueFrom(this.#http.postTodo(/api/todos, variables)), })) // 需要时调用 this.mutation.mutate({ title: 学习 Angular Query }) }实现要点与查询类似computed(injectMutationFn)使选项可响应信号mutateFnSignal封装observer.mutate(...).catch(noop)内部吞掉未处理的 Promise rejection当state.isPending时注册 pending taskmutation 结束时释放throwOnError逻辑同样通过ngZone.onError上报。返回对象中mutate与mutateAsync均可用其中mutateAsync即底层的result.mutate返回 Promise 的版本。injectInfiniteQuery无限滚动import { injectInfiniteQuery } from tanstack/angular-query-experimental class ProjectsComponent { projects injectInfiniteQuery(() ({ queryKey: [projects], queryFn: ({ pageParam }) fetchProjects(pageParam), initialPageParam: 0, getNextPageParam: (lastPage) lastPage.nextCursor, })) }injectInfiniteQuery复用createBaseQuery并传入InfiniteQueryObserverinject-infinite-query.ts支持“增量加载更多数据到已有数据集”或“无限滚动”配合fetchNextPage/fetchPreviousPage方法与data.pages展开渲染。仓库 examples/angular 下有infinite-query-with-max-pages等示例目录可参考完整工程配置。类型安全的选项共享queryOptions与mutationOptionsquery-options.ts 提供queryOptions工具用于类型安全地共享和复用查询选项。其特殊之处在于返回的queryKey会被打上queryFn返回值的类型标签QueryKeyWithDataTag从而让queryClient.getQueryData(queryKey)等调用自动推断出数据类型const { queryKey } queryOptions({ queryKey: [key], queryFn: () Promise.resolve(5), // ^? Promisenumber }) const queryClient new QueryClient() const data queryClient.getQueryData(queryKey) // ^? number | undefined同样地mutationOptions用于类型安全地定义 mutation 选项见 mutation-options.ts。七、开发者工具启用方式已在初始化章节介绍provideTanStackQuery(new QueryClient(), withDevtools())。其行为细节默认仅开发模式加载shouldLoadToolsSignal中未显式配置loadDevtools时以isDevMode()为准with-devtools.ts支持响应式配置withDevtools接受一个返回DevtoolsOptions的函数并支持通过options.deps注入依赖配置变化会通过effect实时同步setClient、setPosition、setTheme等渲染位置默认渲染在body中挂载的div.tsqd-parent-container下如需更精细控制渲染位置可改用injectDevtoolsPanel见 devtools-panel将 Devtools 嵌入到自己的组件中生产环境零成本由于 exports 条件导出生产构建引用的是 stub.ts 空实现不会打入实际 Devtools 代码。八、与仓库其他部分的联系框架适配层本包依赖tanstack/query-coreworkspace:*所有观察者QueryObserver、MutationObserver、InfiniteQueryObserver与缓存逻辑都来自 query-coreAngular 适配层只负责把核心结果桥接到 Signals 与 NgZone测试佐证src/__tests__下的provide-tanstack-query.test.ts、inject-query.test.ts、inject-mutation.test.ts、signal-proxy.test.ts、with-devtools.test.ts等测试覆盖了初始化、注入、信号代理与 Devtools 的核心行为可作为理解 API 行为边界的参考完整示例工程examples/angular 目录包含basic、router、auto-refetching、optimistic-updates、infinite-query-with-max-pages、pagination、rxjs等可运行示例官方文档docs/framework/angular 下提供overview、quick-start、installation以及guides/reference等更深入的主题文档如查询、变更、无限查询、Devtools、SSR 等docs/reference/QueryClient.md 则覆盖核心类的完整 API 参考。九、小结Angular Query 的价值在于把“服务端状态”的管理从组件样板代码中剥离出来provideTanStackQuery负责全局缓存与配置injectQuery/injectInfiniteQuery/injectMutation以 Signals 原生方式暴露响应式结果signalProxy让结果字段无需.value即可在模板中自动解包computedeffectnotifyManager.batchCallsNgZone的组合保证了响应式更新与变更检测的协调。从 README 的 Quick Start 出发结合源码中createBaseQuery与signalProxy的实现你可以在自己的 Angular 16 应用中快速落地一套高性能、可取消、自带缓存的异步数据层。【免费下载链接】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),仅供参考