深入理解 Lit Query 的 queryClientContext:在组件树中共享 QueryClient 的上下文机制 📅 发布时间:2026/9/9 12:39:02 👁 浏览次数: 深入理解 Lit Query 的 queryClientContext在组件树中共享 QueryClient 的上下文机制【免费下载链接】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导读queryClientContext是tanstack/lit-query本仓库packages/lit-query中用于在 Lit 组件树中向下分发QueryClient实例的核心 Lit Context 键。本文以 queryClientContext 参考文档 为主体结合其源码实现、QueryClientProvider与控制器绑定流程讲解它如何让宿主组件通过上下文机制共享同一个查询客户端以及绝大多数应用应如何通过QueryClientProvider使用它而非直接操作该 context 键。读完你将理解 Lit Query 的依赖注入模型、context 解析的完整链路以及如何在自己的 Lit 应用中正确地组装查询客户端。一、queryClientContext 是什么1.1 API 签名与用途从参考文档可见queryClientContext的类型签名为const queryClientContext: object;它被定义在 packages/lit-query/src/context.ts:11其实际声明为import { createContext } from lit/context import type { QueryClient } from tanstack/query-core export const queryClientContext createContextQueryClient( Symbol.for(tanstack-query-client), )结合文档说明可以总结它的核心定位它是一个 Lit Context 键由lit/context的createContext创建携带的上下文值类型是tanstack/query-core的QueryClient它被QueryClientProvider提供端和「宿主绑定host-boundAPI」消费端共同使用实现在整个 DOM 子树中共享同一个QueryClient文档明确建议大多数应用应当使用QueryClientProvider而不是直接操作该 context 键。需要特别说明的是queryClientContext的类型签名在参考文档中显示为object这是 TypeScript 类型生成工具对 context 对象类型的宽泛化表示并不代表实际共享值是一个空对象——真正通过该键共享的上下文值类型是QueryClient源码中的泛型createContextQueryClient才是准确约束。1.2 为什么需要 Symbol.for 作为上下文标识createContext的第二个参数传入Symbol.for(tanstack-query-client)。使用全局注册表符号的好处是即使应用在多个地方以不同模块实例引入tanstack/lit-query只要 symbol 描述符一致lit/context的ContextProvider与ContextConsumer之间仍能匹配到同一个上下文键避免因模块重复打包导致依赖注入失效。1.3 全局导出位置queryClientContext从包入口统一导出源码见 packages/lit-query/src/index.ts:7-14export { getDefaultQueryClient, queryClientContext, registerDefaultQueryClient, resolveQueryClient, unregisterDefaultQueryClient, useQueryClient, } from ./context.js也就是说应用可以import { queryClientContext } from tanstack/lit-query拿到这个键用于自行构造基于lit/context的ContextProvider/ContextConsumer。二、谁在「提供」与「消费」这个上下文2.1 提供端QueryClientProviderqueryClientContext最主要的提供端是QueryClientProvider文档与源码类注释均说明了这一角色实现位于 packages/lit-query/src/QueryClientProvider.ts:64。它是一个继承自LitElement的自定义元素核心机制为export class QueryClientProvider extends LitElement { /** internal */ static properties { client: { attribute: false }, } declare client: QueryClient private readonly contextProvider: ContextProvidertypeof queryClientContext constructor() { super() this.contextProvider new ContextProvider(this, { context: queryClientContext, }) } // ... }要点如下client是 property 而非 attribute。参考文档与类源码注释都强调在 Lit 模板中渲染时要用属性绑定.client${queryClient}而不是属性字符串。构造器中通过lit/context的ContextProvider以queryClientContext为键创建提供器并把当前元素作为上下文宿主。在connectedCallback中调用requireClient()校验client已就绪随后contextProvider.setValue(client)将客户端写入 context再调用mountClient(client)完成client.mount()并注册默认客户端disconnectedCallback则执行对称的卸载流程。若在已连接状态下把client置空会先解绑已挂载的客户端再向活跃消费者通知「提供器已无绑定」并抛出No QueryClient available...错误。该包不会自动注册自定义元素应用需要自行通过customElements.define注册QueryClientProvider本身或其子类。2.2 消费端宿主绑定的控制器另一类使用者是「宿主绑定 API」即createQueryController、createMutationController、createInfiniteQueryController、createQueriesController等返回的 Lit 响应式控制器。这些控制器共同继承自 packages/lit-query/src/controllers/BaseController.ts:15其中完成了对queryClientContext的订阅contextTarget.dispatchEvent( new ContextEvent( queryClientContext, contextTarget as unknown as Element, (value, unsubscribe) { // 校验销毁/连接状态后更新 contextClient 并触发 update }, true, ), )也就是说控制器本身不直接持有全局单例而是让宿主元素派发一个携带queryClientContext键的ContextEvent由最近的ContextProvider响应回调把QueryClient注入控制器。这一设计让同一套 controller 代码既能运行在被QueryClientProvider包裹的子树中也能通过显式传入queryClient独立工作。2.3 解析优先级与状态机从 BaseController.ts 源码可以还原客户端的解析规则显式优先构造控制器时若传入了queryClient参数状态直接进入bound不再发起 context 请求context 兜底未传显式客户端时进入awaiting-context状态并派发ContextEvent收到非undefined值后转为bound缺失兜底若无任何 Provider 响应最终转入missing状态访问结果时抛出缺失错误消息见 context.ts:15-18。这一状态机保证了「在组件树的任意层级创建控制器都能以可预测的方式拿到客户端」并在没有 Provider 的情况下给出清晰的错误而不是静默失败。三、完整实战从 Provider 到消费者的最小可运行结构3.1 安装依赖tanstack/lit-query当前在仓库中标记为实验性 v0.1见 packages/lit-query/README.md:20-23生产使用建议锁定精确版本。安装命令npm install tanstack/lit-query tanstack/query-core lit本仓库内本地开发方式pnpm install pnpm --dir packages/lit-query run build3.2 方案 A定义 Provider 子类官方推荐写法参考文档与 QueryClientProvider 类参考 提供的第一种写法是通过子类预置clientimport { html, LitElement } from lit import { QueryClient, QueryClientProvider } from tanstack/lit-query const queryClient new QueryClient({ defaultOptions: { queries: { retry: false } }, }) class AppQueryProvider extends QueryClientProvider { constructor() { super() this.client queryClient } } customElements.define(app-query-provider, AppQueryProvider) class AppRoot extends LitElement { render() { return htmlapp-query-providertodos-view/todos-view/app-query-provider } } customElements.define(app-root, AppRoot)3.3 方案 B直接在模板中属性绑定第二种写法是直接注册基类本身在渲染模板时用.client属性绑定因为client不是 attribute必须使用属性绑定语法import { html } from lit import { QueryClient, QueryClientProvider } from tanstack/lit-query const queryClient new QueryClient() customElements.define(query-client-provider, QueryClientProvider) const view html query-client-provider .client${queryClient} todos-view/todos-view /query-client-provider 两种方案在效果上等价——都是让query-client-provider成为 DOM 子树的上下文根。区别在于方案 A 把客户端生命周期封装在自定义元素子类内部适合需要复用、便于在组件树顶层一次性配置的场景。3.4 消费者如何使用共享的 QueryClient处于 Provider 子树内的元素其控制器无需感知queryClientContext键本身只需正常创建控制器class UsersView extends LitElement { private readonly users createQueryController(this, { queryKey: [users], queryFn: async () { const response await fetch(/api/users) return response.json() as PromiseArray{ id: string; name: string } }, }) render() { const query this.users() if (query.isPending) return htmlLoading... if (query.isError) return htmlError return htmlul ${query.data?.map((u) htmlli${u.name}/li)} /ul } } customElements.define(users-view, UsersView)完整可运行的参考代码位于仓库根目录examples/lit/basic覆盖 query 与 mutation 基础用法、examples/lit/pagination分页、预取、乐观更新与错误恢复与examples/lit/ssrSSR 脱水/注水流程。运行示例pnpm --dir examples/lit/basic run dev pnpm --dir examples/lit/pagination run dev pnpm --dir examples/lit/ssr run dev四、上下文之外的进程级兜底机制虽然queryClientContext是 DOM 树内向下的主要共享通道context.ts还提供了一套「进程级注册表」兜底机制用于在控制器作用域之外例如普通的函数调用解析当前默认客户端。这些函数同样从包入口导出导出函数作用源码位置registerDefaultQueryClient(client)把客户端登记为进程级默认客户端带引用计数QueryClientProvider连接时自动调用context.ts:32unregisterDefaultQueryClient(client)释放登记QueryClientProvider断开时自动调用只有最后一个使用方断开才真正移除context.ts:45getDefaultQueryClient()返回已登记的默认客户端若同时登记了多个不同客户端则返回undefined避免歧义context.ts:72useQueryClient()返回唯一默认客户端无客户端抛「缺失」错误多个客户端则抛「歧义」错误context.ts:98resolveQueryClient(explicit?)显式传入则直接使用否则回退到useQueryClient()context.ts:118参考文档明确指出「大多数应用使用QueryClientProvider而非直接操作 context 键」与此对应的最佳实践是在组件树内一律依赖 Provider 控制器自动解析只有在非组件环境如普通工具函数需要取客户端时才考虑useQueryClient/resolveQueryClient且应保证同一时刻只有一个 Provider 连接在 DOM 上。两条关键错误消息也在 context.ts:15-18 中定义是排查问题的重要信号No QueryClient available. Pass one explicitly or render within QueryClientProvider.—— 没有任何 Provider 或显式客户端Multiple QueryClients are mounted. Pass one explicitly instead of relying on global QueryClient helpers.—— 同时挂载了多个不同客户端进程级兜底查找产生歧义此时应改为显式传参。从源码可以推断出实现细节注册表用MapQueryClient, number记录引用计数因此同一个客户端被多个 Provider 同时使用时只要还有至少一个 Provider 在连接默认客户端就不会被注销。五、生命周期与解析链路的源码级验证5.1 Provider 的挂载/卸载契约QueryClientProvider.ts:140-165 中的mountClient/unmountClient实现了严谨的契约private mountClient(client: QueryClient): void { if (this.mountedClient client) { return } if (this.mountedClient) { this.unmountClient(this.mountedClient) } client.mount() registerDefaultQueryClient(client) this.mountedClient client }即每个client.mount()必然与一次client.unmount()unregisterDefaultQueryClient配对且切换客户端时会先卸载旧客户端再挂载新客户端杜绝重复挂载。5.2 测试用例印证仓库单元测试 packages/lit-query/src/tests/context-provider.test.ts 对上述行为做了完整的断言验证可直接作为理解本文主题的补充资料注册/注销与公共 helper第 35-50 行Provider 连接后useQueryClient()/resolveQueryClient()均返回该客户端Provider 移除后二者抛出No QueryClient available。显式客户端优先第 52-55 行resolveQueryClient(explicit)总是返回显式传入的客户端。引用计数语义第 57-80 行同一个客户端被两个 Provider 使用时移除其中一个后默认客户端仍可用全部移除后才失效。歧义保护第 82-109 行两个不同客户端同时挂载时getDefaultQueryClient()返回undefineduseQueryClient()/resolveQueryClient()抛出Multiple QueryClients are mounted移除其中一个后恢复可解析。未提供 client 就连接第 111-116 行直接调用connectedCallback抛出缺失错误。断连期间换 client第 118-167 行通过 spy 验证mount/unmount调用次数保证断开状态下替换 client 不会破坏契约。连接后清空 client第 169-210 行消费型控制器会收到错误、查询进入「无客户端」状态refetch也会被拒绝。这些测试把「文档级描述」落到「可复现的行为约束」是后续升级或二次开发时防止回归的重要保障。5.3 小结一条贯穿全文的解析链路从提供到消费queryClientContext的完整链路可概括为QueryClientProvider连接后创建以queryClientContext为键的ContextProvider并setValue写入客户端后代元素中的响应式控制器在hostConnected后派发携带同一键的ContextEventlit/context沿 DOM 树向上匹配最近的 Provider回调把客户端注入控制器并订阅变化若整棵树没有任何 Provider控制器最终进入missing状态current访问或refetch等操作抛出No QueryClient available错误。对绝大多数 Lit Query 应用而言你只需要记住在组件树根部放置一个绑定好QueryClient的QueryClientProvider其余一切由queryClientContext在幕后自动完成。理解这个键与它周围的注册表函数则能帮助你在遇到多实例、无 Provider 或运行时替换客户端等边界场景时快速定位根因。六、相关阅读上下文键本体与兜底注册表context.ts上下文提供端类参考QueryClientProvider上下文提供端实现QueryClientProvider.ts控制器侧 context 消费逻辑BaseController.ts包导出清单index.ts行为契约测试context-provider.test.ts包安装与快速开始README.md官方快速入门文档quick-start.md【免费下载链接】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),仅供参考