From ad8f9f55ea79a4d587aecc69698808da82de3d62 Mon Sep 17 00:00:00 2001 From: somhairle Date: Tue, 21 Apr 2026 20:40:27 +0800 Subject: 二〇二六年四月二十一日 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/_posts/svelte-lazyquery.md | 157 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 source/_posts/svelte-lazyquery.md (limited to 'source/_posts/svelte-lazyquery.md') diff --git a/source/_posts/svelte-lazyquery.md b/source/_posts/svelte-lazyquery.md new file mode 100644 index 00000000..94c33742 --- /dev/null +++ b/source/_posts/svelte-lazyquery.md @@ -0,0 +1,157 @@ +--- +title: 使用 Runed intersection observer 与 tanstack query 实现通用的惰性加载组件 +date: 2025-07-31 18:49:45 +tags: [Technique] +--- + +在前端,一个常见的性能瓶颈是初始加载时请求了用户当前 viewpoint 之外的非必要数据。惰性加载(Lazy Loading)是一种关键策略,它将数据获取推迟到组件进入 viewpoint 时才执行。本文将阐述如何结合使用 Svelte 5、`@tanstack/svelte-query` 和 `runed.dev` 的 `useIntersectionObserver`,构建一个通用的、可复用的惰性加载组件。 + +**核心依赖与环境** + +- **Svelte 5:** 本实现依赖于 Svelte 5 的符文(Runes)特性,它提供了更精细、更直观的状态管理能力。 + +- **@tanstack/svelte-query:** TanStack Query 的 Svelte 适配版,用户获取数据。 + +- **runed.dev:** 一个提供多种 Svelte 5 实用工具的库,本文主要使用其 `useIntersectionObserver`。 + +#### **设计哲学:分离关注点** + +该方案的核心思想是将“何时加载”与“如何加载”这两个关注点进行解耦。 + +1. **何时加载 (When to Load):** 组件的可见性决定了数据加载的时机。我们利用 `Intersection Observer API` 来精确、高效地监听一个元素是否进入 viewpoint。`runed.dev` 库为此提供了名为 `useIntersectionObserver` 的便捷封装。 + +2. **如何加载 (How to Load):** 数据获取、缓存、同步和状态管理的复杂性由 `@tanstack/svelte-query` (TanStack Query) 处理。它提供了一套强大的工具集来管理异步数据。 + + +通过将这两者结合,可以创建一个名为 `LazyQuery` 的抽象组件。该组件内部处理可见性检测,并根据检测结果动态控制 TanStack Query 的执行,而将具体的查询逻辑(`queryFn`)和键(`queryKey`)完全交由使用者定义。 + +#### `LazyQuery` 组件的实现 + +> 目标:封装惰性加载逻辑,并向外暴露一个标准的 TanStack Query 接口。 + +一、对组件的接口类型做如下定义: + +```typescript +import type { CreateQueryOptions, QueryKey } from '@tanstack/svelte-query'; +import type { Snippet } from 'svelte'; + +type Props< + TQueryFnData = unknown, + TError = Error, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey +> = { + /// 一个标准的 TanStack Query 配置对象 + queryOptions: CreateQueryOptions; + + /// Svelte 5 的 `Snippet` 类型,它允许父组件向子组件传递一段可执行的 UI 模板。 + /// 这个 `Snippet` 会接收到 `createQuery` 返回的完整查询对象 `query`, + /// 从而可以访问 `data`, `isLoading`, `error` 等所有状态。 + children: Snippet<{ + query: ReturnType>; + }>; +}; +``` + +二、组件逻辑: + +```html + + +
+ {#snippet children({ query })} + {/snippet} +
+``` + +- 我们创建一个 `div` 元素作为哨兵(sentinel),并用 `bind:this={el}` 将其 DOM 引用绑定到变量 `el`。 +- `useIntersectionObserver` 接收一个返回目标元素的函数 `() => el`。 + - 它返回一个响应式的状态对象,其中 `isIntersecting` 是一个布尔值的符文(rune),当 `div` 元素进入 viewpoint 时为 `true`,否则为 `false`。 +- `rootMargin: '200px'` 是一个优化选项,它会在元素距离 viewpoint 还有 200px 时就触发加载,从而提升用户体验。 + +#### 使用 + +使用 `LazyQuery` 组件非常直观。开发者只需关注数据获取的业务逻辑,而无需关心惰性加载的实现细节,假设有一个获取图表数据的场景: + +```html + + + +
+

Scroll down to see the chart...

+
+ + + {#snippet children({ query })} + {#if query.isLoading} +

Loading chart data...

+ {:else if query.error} +

Error: {query.error.message}

+ {:else if query.data} + query.refetch()} + data={query.data.data} + totalPatients={query.data.data.totalPatients} + /> + {/if} + {/snippet} +
+
+``` + +- 页面初始加载时,`LazyQuery` 组件被渲染,但由于其 `div` 在 viewpoint 之外,`isIntersecting` 为 `false`。 +- `createQuery` 被调用,但因为 `enabled` 条件为 `false`,查询处于禁用状态,不会发起任何网络请求。 +- `children` 片段被渲染,此时 `query.isLoading` 为 `true`(这是 TanStack Query 禁用查询时的初始状态),显示 "Loading chart data..."。 +- 当用户向下滚动,`div` 元素进入 viewpoint(或进入 `200px` 的预加载区域)。 +- `useIntersectionObserver` 将 `isIntersecting` 的值更新为 `true`。 +- 这个变化被 `createQuery` 的 `enabled` 访问器捕获,查询被自动激活,`queryFn` 开始执行。 +- TanStack Query 负责管理后续的状态变化(`isFetching`, `data`, `error`),并驱动 `children` 片段内的 UI 自动更新。 + +#### 参考 + +- https://runed.dev/docs/utilities/use-intersection-observer +- https://tanstack.com/table/latest/docs/introduction -- cgit v1.2.3