ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

@tanstack/lit-query QueryClientProvider 全面解析:向 Lit 组件树注入与共享 QueryClient 的上下文 Provider

@tanstack/lit-query QueryClientProvider 全面解析:向 Lit 组件树注入与共享 QueryClient 的上下文 Provider tanstack/lit-query QueryClientProvider 全面解析向 Lit 组件树注入与共享 QueryClient 的上下文 Provider【免费下载链接】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本指南系统讲解tanstack/lit-query中QueryClientProvider这一 Lit 元素的职责、用法与底层实现。QueryClientProvider是 Lit Query 应用的服务入口——它通过 Lit 的 Context API 把唯一的QueryClient提供给其子树内所有 Query 控制器如createQueryController、createMutationController并同步管理该 client 的 mount / unmount 生命周期。读完本文你将掌握其属性绑定规则、customElements.define注册方式、生命周期校验逻辑、全局默认 client 的注册与反注册机制以及多 Provider 并存时的行为边界。QueryClientProvider 是什么位置、职责与工作方式QueryClientProvider是tanstack/lit-query包源码见 packages/lit-query/src/QueryClientProvider.ts导出的一种Lit 元素LitElement 子类它的角色是通过 Lit context 向所有后代 Lit Query 控制器提供一个QueryClient。与 React Query 中组件式的QueryClientProvider类似Lit 版本同样采用「Provider 在上层包裹、子树中的控制器共享同一 client」的结构但底层依赖的是lit/context的 Context API而非 React ContextProvider 在自身构造阶段创建一个ContextProvider其 context key 为queryClientContext当 Provider 元素被连接到 DOMconnectedCallback时把client属性的值写入 context子树中的 Query 控制器通过createQueryController、createMutationController等 host-bound API 消费该 context得到同一个QueryClient。从包导出结构看packages/lit-query/src/index.tsQueryClientProvider与queryClientContext、registerDefaultQueryClient、unregisterDefaultQueryClient、getDefaultQueryClient、resolveQueryClient、useQueryClient等 API 一同从./context.js与./QueryClientProvider.js导出共同构成 client 分发体系。两个必须记住的核心约定原文档docs/framework/lit/reference/classes/QueryClientProvider.md中明确了两条使用铁律直接由源码落实client是属性property不是属性attribute。在 Lit 模板中渲染该元素时必须使用属性绑定语法.client${queryClient}。如果写成client${queryClient}这样的 attribute 绑定传入的会被强制字符串化无法正确工作。这一约定由源码中的声明保证// packages/lit-query/src/QueryClientProvider.ts static properties { client: { attribute: false }, }{ attribute: false }意味着 Lit 不会把client映射为 HTML attribute它只作为一个 JS 属性存在。该类默认不注册为自定义元素。包本身不会调用customElements.define应用必须自行注册——既可以注册该类的直接实例也可以注册其子类customElements.define(query-client-provider, QueryClientProvider) // 或 customElements.define(app-query-provider, AppQueryProvider)若忘记注册直接使用标签名浏览器会把它当作未知 HTML 元素Provider 逻辑不会执行。快速上手文档中的两种标准用法原文档提供了两段可直接落地的示例均以「todos-view」作为需要 query 数据的后代组件。下面保留并给出完整可运行形态。方式一定义子类并在构造器中注入 clientimport { html, LitElement } from lit import { QueryClient, QueryClientProvider } from tanstack/lit-query const queryClient new QueryClient() 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 } }该写法把「使用哪个 client」封装进子类构造器之后在模板中无需再传值适合全站统一 client 的入口场景。方式二直接注册类本身模板中属性绑定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 第二种方式把 client 的提供点放在模板层语义更显式——todos-view可以是任意深度的嵌套后代元素。注意若 Provider 在连接时connect没有携带 client或已连接的 Provider 被清空 client它会直接抛错详见下文「生命周期与校验」因此请确保 client 在元素接入 DOM 前已赋值。仓库真实示例中的用法印证QueryClientProvider在仓库的 lit 示例中被大量使用可以作为最佳实践参考examples/lit/basic/src/main.ts在模块顶层创建new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } })然后class DemoQueryProvider extends QueryClientProvider { constructor() { super() this.client demoQueryClient } protected override createRenderRoot(): HTMLElement | DocumentFragment { return this } } customElements.define(demo-query-provider, DemoQueryProvider)根组件把demo-query-providertanstack-lit-query-demo/tanstack-lit-query-demo/demo-query-provider作为其 render 输出tanstack-lit-query-demo内部则用createQueryController(this, ...)、createMutationController(this, ...)、useIsFetching(this, ...)等控制器消费该 client。examples/lit/basic/src/basic-query.ts同样的「构造器赋值 define」模式配合createRenderRoot覆写为直接渲染到this避免 shadow DOM 影响样式与测试定位。examples/lit/basic/src/lifecycle-contract.ts 中出现ContractProviderA/ContractProviderB两个 Provider 变体用于演示 client 切换对生命周期契约的影响。一个值得注意的共性示例中 Provider 的 render 输出是其子节点slot/slot而 Provider 子类本身常覆写createRenderRoot返回this。后者是为了把 Provider 的slot直接放进 document保证影子边界内外的后代元素都能正确被 context 覆盖。API 一览继承、构造与属性QueryClientProvider的公开 API 形状如下项详情ExtendsLitElementConstructornew QueryClientProvider(): QueryClientProvider重写LitElement.constructorPropertyclient: QueryClient—— 提供给后代控制器及全局 fallback helper 的QueryClient属性绑定语法.client${queryClient}注册要求默认未注册为自定义元素需应用自行customElements.defineClass 级 properties 声明client: { attribute: false }client 属性的双重作用源码对client属性的注释是TheQueryClientprovided to descendant controllers and global fallback helpers while this provider is connected.「descendant controllers」指通过lit/context消费queryClientContext的 Query 控制器「global fallback helpers」则指context.ts中维护的进程级默认 client 注册表——即useQueryClient()/resolveQueryClient()等不带显式参数的 API 所读取的默认 client。生命周期与校验connected / disconnected / update 的完整状态机QueryClientProvider的生命周期逻辑集中在 packages/lit-query/src/QueryClientProvider.ts 中以下是逐段拆解connectedCallback连接时挂载connectedCallback(): void { super.connectedCallback() const client this.requireClient() // 无 client 直接抛错 this.contextProvider.setValue(client) // 写入 lit context this.mountClient(client) // client.mount() 注册默认 client }requireClient()在this.client为空时抛出错误消息No QueryClient available. Pass one explicitly or render within QueryClientProvider.由 packages/lit-query/src/context.ts 的createMissingQueryClientError统一构造。若客户端未传 client 就把 Provider 插入 DOM该错误即被触发。mountClient / unmountClient真正的资源生命周期mountClient与unmountClient是 Provider 与 QueryClient 生命周期对齐的核心private mountClient(client: QueryClient): void { if (this.mountedClient client) return // 同一 client 幂等 if (this.mountedClient) this.unmountClient(this.mountedClient) client.mount() // QueryClient 开始工作 registerDefaultQueryClient(client) // 登记为全局默认 this.mountedClient client } private unmountClient(client?: QueryClient): void { if (!client) return client.unmount() // 停止该 client unregisterDefaultQueryClient(client) // 撤销全局登记 if (this.mountedClient client) this.mountedClient undefined }这里的client.mount()/client.unmount()是tanstack/query-core中QueryClient自身的方法仓库包 packages/query-core 提供实现。mount()会触发缓存中未完成 query 的绑定与清理调度使该 client 真正具备执行与派发能力unmount()则停止其活动。因此「Provider 何时在 DOM 中」直接决定了 QueryClient 何时被激活——这正是文档所述“while this provider is connected”的含义。disconnectedCallback断开时卸载disconnectedCallback(): void { this.unmountClient(this.mountedClient) super.disconnectedCallback() }Provider 从 DOM 移除时会卸载当前 clientunmount 撤销全局默认登记确保没有悬挂的全局默认 client 残留。willUpdateclient 热切换时的精细处理当client属性变化时例如应用运行时切换到另一 client 实例willUpdate中有一套精心设计的处理const nextClient this.client if (!nextClient) { if (this.isConnected) { this.unmountClient(this.mountedClient) // Sentinel: 通知仍存活的消费者Provider 已无 client this.contextProvider.setValue(undefined as unknown as QueryClient) throw createMissingQueryClientError() // 更新会因此失败 } return } // 正常切换卸载旧 client写入新值并挂载新 client关键点清空 client若在已连接状态下把client置空Provider 会先卸载当前 client再向 context 写入一个 sentinelundefined值让所有还在消费的控制器感知到“client 已失效”最后抛出缺少 client 的错误测试中表现为provider.updateComplete被 reject见下文切换 client若新旧 client 不同先卸载旧 client然后setValue(nextClient)更新 context 并挂载新 client消费方会通过 context 的订阅回调收到值变更。render透传子节点render(): TemplateResult { return htmlslot/slot }Provider 本身不渲染任何 UI仅负责透传 slot 内容。底层 context 机制从 key 到全局默认 client在 packages/lit-query/src/context.ts 中可以看到支撑 Provider 的全部基础设施export const queryClientContext createContextQueryClient( Symbol.for(tanstack-query-client), )context key 使用全局 SymbolSymbol.for(tanstack-query-client)这保证了跨多个副本/模块加载同一 key多个 Provider 间不会因模块实例化次数不同而产生“同一 context 不同 key”的分裂。全局默认 client 的引用计数注册表registerDefaultQueryClient/unregisterDefaultQueryClient用MapQueryClient, number做引用计数const registeredClients new MapQueryClient, number() let defaultClient: QueryClient | undefined export function registerDefaultQueryClient(client: QueryClient): void { registeredClients.set(client, (registeredClients.get(client) ?? 0) 1) defaultClient client }含义是多个 Provider 使用同一个 client 实例时只有最后一个 Provider 断开后该 client 才真正被“注销”。unregister端在计数归零后才从registeredClients删除并把默认 client 回退到注册表中最后一个剩余的 client。getDefaultQueryClient()的语义也很有意思export function getDefaultQueryClient(): QueryClient | undefined { if (registeredClients.size 1) return undefined // 多个不同 client - 视为不明确 return defaultClient }即只要注册了多个不同的 client多 Provider 且 client 不同就不再返回默认 client避免全局 helper 拿到歧义值。对外查询/解析 APIuseQueryClient()返回唯一注册的默认 client无注册或注册多个不同 client 时抛错。错误消息在 packages/lit-query/src/context.ts 中定义为无 clientNo QueryClient available. Pass one explicitly or render within QueryClientProvider.多个 clientMultiple QueryClients are mounted. Pass one explicitly instead of relying on global QueryClient helpers.resolveQueryClient(explicit?)explicit ?? useQueryClient()显式传入的 client 优先否则回退到默认 client。这两个 API 均在包入口 packages/lit-query/src/index.ts 导出Query 控制器解析 client 时也会使用相同的错误/解析约定。下游控制器如何消费 contextBaseController 的解析状态机Provider 一侧写入 context 后消费方是 packages/lit-query/src/controllers/BaseController.ts 中的BaseControllercreateQueryController、createMutationController、createQueriesController、createInfiniteQueryController均继承自它。关键机制BaseController.ts显式 client 优先控制器构造时可传入显式queryClient参数此时状态为bound直接绑定否则走 context 解析hostConnected时调用beginContextResolution()进入awaiting-context状态然后dispatchContextRequest在宿主元素上派发new ContextEvent(queryClientContext, ...)请求ContextProvider收到请求后回调将client值返回给控制器控制器据此从awaiting-context转为bound若请求无人响应没有祖先 Provider在queueInitialContextResolutionCompletion的微任务中转入missing状态此时访问current会抛出No QueryClient available。BaseController通过connectionAttempt计数与“attempt 是否仍然匹配”的判断来防止异步 context 解析结果过期例如组件在等待响应期间已被移除并以queryClientResolutionStatepre-connect|awaiting-context|bound|missing四个状态描述解析过程从而可以区分“还没有上下文”和“确定没有 client”两种情形。这就是为什么文档强调 Provider 必须包裹消费组件控制器只有在 DOM 树中向上能“看到”某个祖先 Provider 的 context 值时才能完成 client 解析否则控制器对.current的读取会抛错。多 Provider 语义与错误场景测试用例的验证仓库的单元测试 packages/lit-query/src/tests/context-provider.test.ts 直接覆盖了文档承诺的各类行为可作为行为契约的权威依据测试场景验证的行为「registers and unregisters the default query client for public helpers」Provider 连接后useQueryClient()/resolveQueryClient()返回该 client移除 Provider 后抛出/No QueryClient available/「prefers an explicit client in resolveQueryClient」resolveQueryClient(explicit)恒返回显式 client「keeps the default client registered until the last provider using it disconnects」两个 Provider 共用一个 client先移除其一默认 client 仍可用移除最后一个后才报缺失「throws when multiple different providers make global lookup ambiguous」两个 Provider 挂不同 clientgetDefaultQueryClient()返回undefineduseQueryClient()/resolveQueryClient()抛/Multiple QueryClients are mounted/移除其中一个后恢复可用「requires an explicit client before connect」未赋 client 的 Provider 调用connectedCallback()直接抛/No QueryClient available/「provider swap while disconnected preserves mount/unmount contract」用vi.spyOn计数验证连接时mount一次、断开时unmount一次、断开期间换 client 不触发 mount、重连才挂载新 client「invalid connected client updates tear down the mounted client before surfacing the error」已连 Provider 的client被置空后先卸载旧 client再抛错updateCompletereject全局默认 client 随即清空且仍存活的消费者查询会被提示缺 client这些用例共同说明三条文档级结论同一 client 可被多个 Provider 共享全局默认在“最后一个引用断开”前持续有效不同 client 同时在线会使全局 helper 变“歧义”而主动失败推动用户改用显式传参或 per-provider contextProvider 的 mount/unmount 与 client 的挂载严格成对即使 client 在断开期间被切换或清空也不会破坏计数契约。总结与使用建议从使用与实现两个层面总结QueryClientProvider它是一个数据分发节点而非 UI 组件只渲染slot真正价值在于「挂载时把client推入queryClientContext断开时收回」子树的 query 控制器由此拿到同一实例。务必使用属性绑定.client并在元素接入 DOM 前完成赋值否则会在连接或更新时抛出No QueryClient available...。记得customElements.define否则标签不会成为可用的 Provider。全局 helperuseQueryClient/resolveQueryClient只在 Provider 已连接时可用多 client 并存时应优先显式传参或依赖 context而不是依赖全局默认。排查问题时可以同时阅读三处代码Provider 本体 packages/lit-query/src/QueryClientProvider.ts、context 基础设施 packages/lit-query/src/context.ts、控制器侧解析逻辑 packages/lit-query/src/controllers/BaseController.ts再配合 packages/lit-query/src/tests/context-provider.test.ts 中的行为契约来定位问题。如果想在真实工程中观察整体串联可运行 examples/lit/basic提供 basic-query / mutation / lifecycle-contract 三组页面与 examples/lit/pagination 等示例它们都把QueryClientProvider子类作为应用根包裹层是理解 Provider 用法的完整活样本。【免费下载链接】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),仅供参考
返回列表