diff options
| author | muqiuhan <[email protected]> | 2025-07-31 10:50:58 +0000 |
|---|---|---|
| committer | muqiuhan <[email protected]> | 2025-07-31 10:50:58 +0000 |
| commit | 6ba051fa1ba4e23ee47adef5553263ef654e6008 (patch) | |
| tree | e7ce59ac5c0bdb117103f926a9c56c5fad01e547 /2025/07/31/svelte-lazyquery | |
| parent | 17e9059a904c1d9f58063aace4cc4feed4dfc5b7 (diff) | |
| download | blog-6ba051fa1ba4e23ee47adef5553263ef654e6008.tar.gz | |
deploy: ef7c3e415ce43d2b5307bcfdd5c98109faa25721
Diffstat (limited to '2025/07/31/svelte-lazyquery')
| -rw-r--r-- | 2025/07/31/svelte-lazyquery/index.html | 336 |
1 files changed, 336 insertions, 0 deletions
diff --git a/2025/07/31/svelte-lazyquery/index.html b/2025/07/31/svelte-lazyquery/index.html new file mode 100644 index 00000000..ccfa3744 --- /dev/null +++ b/2025/07/31/svelte-lazyquery/index.html @@ -0,0 +1,336 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8"> +<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> +<meta http-equiv="X-UA-Compatible" content="ie=edge"> + + <meta name="author" content="韩暮秋"> + + + <meta name="subtitle" content="暮秋小屋"> + + + <meta name="description" content="这里是暮秋小屋,思念和灵感的寄存处"> + + + <meta name="keywords" content="韩暮秋,MuqiuHan,'Muqiu Han', 'muqiu han', muqiuhan"> + + + + + <title> + + 使用 Runed intersection observer 与 tanstack query 实现通用的惰性加载组件 | + 暮秋小屋 + </title> + + + + <link rel="icon" href="/favicon.ico"> + + + <style> + @font-face { + font-family: CarroisSong; + src: url('/fonts/CarroisSong.ttf'); + } + </style> + + + + <!-- stylesheets list from _config.yml --> + + <link rel="stylesheet" href="/css/style.css"> + + + + + + <!-- scripts list from _config.yml --> + + <script + src="/js/menu.js"></script> + + + + + + <script + src="https://polyfill.alicdn.com/polyfill.js?features=es6"></script> + <script + id="MathJax-script" + async + src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/mathjax/3.2.0/es5/tex-mml-chtml.js"></script> + + + + + <meta name="generator" content="Hexo 6.3.0"></head> + <body> + <div class="mask-border"> + </div> + + <div class="wrapper"> + + <div class="header"> + <div class="flex-container"> + <div class="header-inner"> + <div class="site-brand-container"> + <a href="/"> + + 暮秋小屋 + + </a> + </div> + <div id="menu-btn" class="menu-btn" onclick="toggleMenu()"> + 菜单 + </div> + <nav class="site-nav"> + <ul class="menu-list"> + + + <li class="menu-item"> + <a href="/"> + 主页 + </a> + </li> + + + + <li class="menu-item"> + <a href="/categories/gallery/"> + 日记本 + </a> + </li> + + + + <li class="menu-item"> + <a href="/tags/Medicine/"> + 医学 + </a> + </li> + + + + <li class="menu-item"> + <a href="/tags/Technique/"> + 计算机/互联网 + </a> + </li> + + + + <li class="menu-item"> + <a href="/tags/Life/"> + 生活 + </a> + </li> + + + + <li class="menu-item"> + <a href="/archives"> + 全部 + </a> + </li> + + + + <li class="menu-item"> + <a href="/about"> + 关于 + </a> + </li> + + + + <li class="menu-item search-btn"> + <a href="#">Search</a> + </li> + + </ul> + </nav> + </div> + </div> +</div> + + <div class="main"> + <div class="flex-container"> + <article id="post"> + + + <div class="post-head"> + <div class="post-info"> + <div class="tag-list"> + + + <span class="post-tag"> + <a href="/tags/Technique/"> + Technique + </a> + </span> + + + </div> + <div class="post-title"> + + + 使用 Runed intersection observer 与 tanstack query 实现通用的惰性加载组件 + + + </div> + <span class="post-date"> + Jul 31, 2025 + </span> + </div> + <div class="post-img"> + + <div class="h-line-primary"></div> + + </div> +</div> + <div class="post-content"> + <p>在前端,一个常见的性能瓶颈是初始加载时请求了用户当前 viewpoint 之外的非必要数据。惰性加载(Lazy Loading)是一种关键策略,它将数据获取推迟到组件进入 viewpoint 时才执行。本文将阐述如何结合使用 Svelte 5、<code>@tanstack/svelte-query</code> 和 <code>runed.dev</code> 的 <code>useIntersectionObserver</code>,构建一个通用的、可复用的惰性加载组件。</p> +<p><strong>核心依赖与环境</strong></p> +<ul> +<li><p><strong>Svelte 5:</strong> 本实现依赖于 Svelte 5 的符文(Runes)特性,它提供了更精细、更直观的状态管理能力。</p> +</li> +<li><p><strong>@tanstack/svelte-query:</strong> TanStack Query 的 Svelte 适配版,用户获取数据。</p> +</li> +<li><p><strong>runed.dev:</strong> 一个提供多种 Svelte 5 实用工具的库,本文主要使用其 <code>useIntersectionObserver</code>。</p> +</li> +</ul> +<h4 id="设计哲学:分离关注点"><a href="#设计哲学:分离关注点" class="headerlink" title="设计哲学:分离关注点"></a><strong>设计哲学:分离关注点</strong></h4><p>该方案的核心思想是将“何时加载”与“如何加载”这两个关注点进行解耦。</p> +<ol> +<li><p><strong>何时加载 (When to Load):</strong> 组件的可见性决定了数据加载的时机。我们利用 <code>Intersection Observer API</code> 来精确、高效地监听一个元素是否进入 viewpoint。<code>runed.dev</code> 库为此提供了名为 <code>useIntersectionObserver</code> 的便捷封装。</p> +</li> +<li><p><strong>如何加载 (How to Load):</strong> 数据获取、缓存、同步和状态管理的复杂性由 <code>@tanstack/svelte-query</code> (TanStack Query) 处理。它提供了一套强大的工具集来管理异步数据。</p> +</li> +</ol> +<p>通过将这两者结合,可以创建一个名为 <code>LazyQuery</code> 的抽象组件。该组件内部处理可见性检测,并根据检测结果动态控制 TanStack Query 的执行,而将具体的查询逻辑(<code>queryFn</code>)和键(<code>queryKey</code>)完全交由使用者定义。</p> +<h4 id="LazyQuery-组件的实现"><a href="#LazyQuery-组件的实现" class="headerlink" title="LazyQuery 组件的实现"></a><code>LazyQuery</code> 组件的实现</h4><blockquote> +<p>目标:封装惰性加载逻辑,并向外暴露一个标准的 TanStack Query 接口。</p> +</blockquote> +<p>一、对组件的接口类型做如下定义:</p> +<figure class="highlight typescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="keyword">type</span> { <span class="title class_">CreateQueryOptions</span>, <span class="title class_">QueryKey</span> } <span class="keyword">from</span> <span class="string">'@tanstack/svelte-query'</span>;</span><br><span class="line"><span class="keyword">import</span> <span class="keyword">type</span> { <span class="title class_">Snippet</span> } <span class="keyword">from</span> <span class="string">'svelte'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> <span class="title class_">Props</span><</span><br><span class="line"> <span class="title class_">TQueryFnData</span> = <span class="built_in">unknown</span>,</span><br><span class="line"> <span class="title class_">TError</span> = <span class="title class_">Error</span>,</span><br><span class="line"> <span class="title class_">TData</span> = <span class="title class_">TQueryFnData</span>,</span><br><span class="line"> <span class="title class_">TQueryKey</span> <span class="keyword">extends</span> <span class="title class_">QueryKey</span> = <span class="title class_">QueryKey</span></span><br><span class="line">> = {</span><br><span class="line"> <span class="comment">/// 一个标准的 TanStack Query 配置对象</span></span><br><span class="line"> <span class="attr">queryOptions</span>: <span class="title class_">CreateQueryOptions</span><<span class="title class_">TQueryFnData</span>, <span class="title class_">TError</span>, <span class="title class_">TData</span>, <span class="title class_">TQueryKey</span>>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/// Svelte 5 的 `Snippet` 类型,它允许父组件向子组件传递一段可执行的 UI 模板。</span></span><br><span class="line"> <span class="comment">/// 这个 `Snippet` 会接收到 `createQuery` 返回的完整查询对象 `query`,</span></span><br><span class="line"> <span class="comment">/// 从而可以访问 `data`, `isLoading`, `error` 等所有状态。</span></span><br><span class="line"> <span class="attr">children</span>: <span class="title class_">Snippet</span><{</span><br><span class="line"> <span class="attr">query</span>: <span class="title class_">ReturnType</span><<span class="keyword">typeof</span> createQuery<<span class="title class_">TQueryFnData</span>, <span class="title class_">TError</span>, <span class="title class_">TData</span>, <span class="title class_">TQueryKey</span>>>;</span><br><span class="line"> }>;</span><br><span class="line">};</span><br></pre></td></tr></table></figure> + +<p>二、组件逻辑:</p> +<figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">script</span> <span class="attr">lang</span>=<span class="string">"ts"</span>></span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">import</span> { createQuery, type <span class="title class_">CreateQueryOptions</span>, type <span class="title class_">QueryKey</span> } <span class="keyword">from</span> <span class="string">'@tanstack/svelte-query'</span>;</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">import</span> { useIntersectionObserver } <span class="keyword">from</span> <span class="string">'runed'</span>;</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">import</span> type { <span class="title class_">Snippet</span> } <span class="keyword">from</span> <span class="string">'svelte'</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="comment">// [上述的类型定义]</span></span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">let</span> { queryOptions, children }: <span class="title class_">Props</span> = $props();</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">let</span> <span class="attr">el</span>: <span class="title class_">Element</span>;</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">const</span> { isIntersecting } = <span class="title function_">useIntersectionObserver</span>(</span></span><br><span class="line"><span class="language-javascript"> <span class="function">() =></span> el,</span></span><br><span class="line"><span class="language-javascript"> {</span></span><br><span class="line"><span class="language-javascript"> <span class="attr">rootMargin</span>: <span class="string">'200px'</span>,</span></span><br><span class="line"><span class="language-javascript"> }</span></span><br><span class="line"><span class="language-javascript"> );</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">const</span> query = <span class="title function_">createQuery</span>({</span></span><br><span class="line"><span class="language-javascript"> ...queryOptions,</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">get</span> <span class="title function_">enabled</span>() {</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">return</span> ($state.<span class="title function_">is</span>(isIntersecting) && (queryOptions.<span class="property">enabled</span> ?? <span class="literal">true</span>));</span></span><br><span class="line"><span class="language-javascript"> }</span></span><br><span class="line"><span class="language-javascript"> });</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag"></<span class="name">script</span>></span></span><br><span class="line"></span><br><span class="line"><span class="tag"><<span class="name">div</span> <span class="attr">bind:this</span>=<span class="string">{el}</span>></span></span><br><span class="line"> {#snippet children({ query })}</span><br><span class="line"> {/snippet}</span><br><span class="line"><span class="tag"></<span class="name">div</span>></span></span><br></pre></td></tr></table></figure> + +<ul> +<li>我们创建一个 <code>div</code> 元素作为哨兵(sentinel),并用 <code>bind:this={el}</code> 将其 DOM 引用绑定到变量 <code>el</code>。</li> +<li><code>useIntersectionObserver</code> 接收一个返回目标元素的函数 <code>() => el</code>。<ul> +<li>它返回一个响应式的状态对象,其中 <code>isIntersecting</code> 是一个布尔值的符文(rune),当 <code>div</code> 元素进入 viewpoint 时为 <code>true</code>,否则为 <code>false</code>。</li> +</ul> +</li> +<li><code>rootMargin: '200px'</code> 是一个优化选项,它会在元素距离 viewpoint 还有 200px 时就触发加载,从而提升用户体验。</li> +</ul> +<h4 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h4><p>使用 <code>LazyQuery</code> 组件非常直观。开发者只需关注数据获取的业务逻辑,而无需关心惰性加载的实现细节,假设有一个获取图表数据的场景:</p> +<figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">script</span> <span class="attr">lang</span>=<span class="string">"ts"</span>></span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">import</span> { <span class="title class_">QueryClientProvider</span>, <span class="title class_">QueryClient</span> } <span class="keyword">from</span> <span class="string">'@tanstack/svelte-query'</span>;</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">import</span> <span class="title class_">LazyQuery</span> <span class="keyword">from</span> <span class="string">'./LazyQuery.svelte'</span>;</span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">import</span> <span class="title class_">CopdGoldGradingChart</span> <span class="keyword">from</span> <span class="string">'./CopdGoldGradingChart.svelte'</span>;</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">const</span> queryClient = <span class="keyword">new</span> <span class="title class_">QueryClient</span>();</span></span><br><span class="line"><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"> <span class="comment">// Define the query configuration object, just like with a standard `createQuery`.</span></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">const</span> queryOptions = {</span></span><br><span class="line"><span class="language-javascript"> <span class="attr">queryKey</span>: [<span class="string">'goldGradingData'</span>],</span></span><br><span class="line"><span class="language-javascript"> <span class="attr">queryFn</span>: <span class="title function_">async</span> () => {</span></span><br><span class="line"><span class="language-javascript"> <span class="comment">// Simulate a network request</span></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">await</span> <span class="keyword">new</span> <span class="title class_">Promise</span>(<span class="function"><span class="params">resolve</span> =></span> <span class="built_in">setTimeout</span>(resolve, <span class="number">1500</span>));</span></span><br><span class="line"><span class="language-javascript"> <span class="comment">// In a real application, this would be an API call</span></span></span><br><span class="line"><span class="language-javascript"> <span class="comment">// const response = await fetch('/api/gold-grading');</span></span></span><br><span class="line"><span class="language-javascript"> <span class="comment">// return await response.json();</span></span></span><br><span class="line"><span class="language-javascript"> <span class="keyword">return</span> { <span class="attr">data</span>: { <span class="attr">totalPatients</span>: <span class="number">1234</span>, <span class="attr">chartPoints</span>: [<span class="comment">/* ... */</span>] } };</span></span><br><span class="line"><span class="language-javascript"> }</span></span><br><span class="line"><span class="language-javascript"> };</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag"></<span class="name">script</span>></span></span><br><span class="line"></span><br><span class="line"><span class="tag"><<span class="name">QueryClientProvider</span> <span class="attr">client</span>=<span class="string">{queryClient}</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">div</span> <span class="attr">style</span>=<span class="string">"height: 200vh;"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">p</span>></span>Scroll down to see the chart...<span class="tag"></<span class="name">p</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">div</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">LazyQuery</span> {<span class="attr">queryOptions</span>}></span></span><br><span class="line"> {#snippet children({ query })}</span><br><span class="line"> {#if query.isLoading}</span><br><span class="line"> <span class="tag"><<span class="name">p</span>></span>Loading chart data...<span class="tag"></<span class="name">p</span>></span></span><br><span class="line"> {:else if query.error}</span><br><span class="line"> <span class="tag"><<span class="name">p</span>></span>Error: {query.error.message}<span class="tag"></<span class="name">p</span>></span></span><br><span class="line"> {:else if query.data}</span><br><span class="line"> <span class="tag"><<span class="name">CopdGoldGradingChart</span></span></span><br><span class="line"><span class="tag"> <span class="attr">isLoading</span>=<span class="string">{query.isFetching}</span></span></span><br><span class="line"><span class="tag"> <span class="attr">error</span>=<span class="string">{query.error}</span></span></span><br><span class="line"><span class="tag"> <span class="attr">refetch</span>=<span class="string">{()</span> =></span> query.refetch()}</span><br><span class="line"> data={query.data.data}</span><br><span class="line"> totalPatients={query.data.data.totalPatients}</span><br><span class="line"> /></span><br><span class="line"> {/if}</span><br><span class="line"> {/snippet}</span><br><span class="line"> <span class="tag"></<span class="name">LazyQuery</span>></span></span><br><span class="line"><span class="tag"></<span class="name">QueryClientProvider</span>></span></span><br></pre></td></tr></table></figure> + +<ul> +<li>页面初始加载时,<code>LazyQuery</code> 组件被渲染,但由于其 <code>div</code> 在 viewpoint 之外,<code>isIntersecting</code> 为 <code>false</code>。</li> +<li><code>createQuery</code> 被调用,但因为 <code>enabled</code> 条件为 <code>false</code>,查询处于禁用状态,不会发起任何网络请求。</li> +<li><code>children</code> 片段被渲染,此时 <code>query.isLoading</code> 为 <code>true</code>(这是 TanStack Query 禁用查询时的初始状态),显示 “Loading chart data…”。</li> +<li>当用户向下滚动,<code>div</code> 元素进入 viewpoint(或进入 <code>200px</code> 的预加载区域)。</li> +<li><code>useIntersectionObserver</code> 将 <code>isIntersecting</code> 的值更新为 <code>true</code>。</li> +<li>这个变化被 <code>createQuery</code> 的 <code>enabled</code> 访问器捕获,查询被自动激活,<code>queryFn</code> 开始执行。</li> +<li>TanStack Query 负责管理后续的状态变化(<code>isFetching</code>, <code>data</code>, <code>error</code>),并驱动 <code>children</code> 片段内的 UI 自动更新。</li> +</ul> +<h4 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h4><ul> +<li><a target="_blank" rel="noopener" href="https://runed.dev/docs/utilities/use-intersection-observer">https://runed.dev/docs/utilities/use-intersection-observer</a></li> +<li><a target="_blank" rel="noopener" href="https://tanstack.com/table/latest/docs/introduction">https://tanstack.com/table/latest/docs/introduction</a></li> +</ul> + +</div> + +<script> + window.onload = detectors(); +</script> + <div class="post-footer"> + <div class="h-line-primary"></div> + <nav class="post-nav"> + <div class="prev-item"> + + </div> + <div class="next-item"> + + <div class="icon arrow-right"></div> + <div class="post-link"> + <a href="/2025/07/29/nestjs-nested-object-dto/">Next</a> + </div> + + </div> + </nav> +</div> + + + <div class="post-comment"> + + + + + + + +</div> + + +</article> + </div> + </div> + + <div class="footer"> + <div class="flex-container"> + <div class="footer-text"> + + + 韩暮秋 | + + + 希望路过的人可以添点柴火让这里暖和点 + + </div> + </div> +</div> + + </div> + + + <div class="search-popup"> + <div class="search-popup-overlay"> + </div> + <div class="search-popup-window" > + <div class="search-header"> + <div class="search-input-container"> + <input autocomplete="off" autocapitalize="off" maxlength="80" + placeholder="Search Anything" spellcheck="false" + type="search" class="search-input"> + </div> + <div class="search-close-btn"> + <div class="icon close-btn"></div> + </div> + </div> + <div class="search-result-container"> + </div> + </div> +</div> + +<script> + const searchConfig = { + path : "/search.xml", + top_n_per_article: "1", + unescape : "false", + trigger: "auto", + preload: "false" + } +</script> +<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/search.js"></script> +<script src="/js/search.js"></script> + + + + </body> +</html> |
