diff options
Diffstat (limited to 'source/_posts/svelte-lazyquery.md')
| -rw-r--r-- | source/_posts/svelte-lazyquery.md | 157 |
1 files changed, 157 insertions, 0 deletions
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<TQueryFnData, TError, TData, TQueryKey>; + + /// Svelte 5 的 `Snippet` 类型,它允许父组件向子组件传递一段可执行的 UI 模板。 + /// 这个 `Snippet` 会接收到 `createQuery` 返回的完整查询对象 `query`, + /// 从而可以访问 `data`, `isLoading`, `error` 等所有状态。 + children: Snippet<{ + query: ReturnType<typeof createQuery<TQueryFnData, TError, TData, TQueryKey>>; + }>; +}; +``` + +二、组件逻辑: + +```html +<script lang="ts"> + import { createQuery, type CreateQueryOptions, type QueryKey } from '@tanstack/svelte-query'; + import { useIntersectionObserver } from 'runed'; + import type { Snippet } from 'svelte'; + + // [上述的类型定义] + + let { queryOptions, children }: Props = $props(); + + let el: Element; + const { isIntersecting } = useIntersectionObserver( + () => el, + { + rootMargin: '200px', + } + ); + + const query = createQuery({ + ...queryOptions, + get enabled() { + return ($state.is(isIntersecting) && (queryOptions.enabled ?? true)); + } + }); +</script> + +<div bind:this={el}> + {#snippet children({ query })} + {/snippet} +</div> +``` + +- 我们创建一个 `div` 元素作为哨兵(sentinel),并用 `bind:this={el}` 将其 DOM 引用绑定到变量 `el`。 +- `useIntersectionObserver` 接收一个返回目标元素的函数 `() => el`。 + - 它返回一个响应式的状态对象,其中 `isIntersecting` 是一个布尔值的符文(rune),当 `div` 元素进入 viewpoint 时为 `true`,否则为 `false`。 +- `rootMargin: '200px'` 是一个优化选项,它会在元素距离 viewpoint 还有 200px 时就触发加载,从而提升用户体验。 + +#### 使用 + +使用 `LazyQuery` 组件非常直观。开发者只需关注数据获取的业务逻辑,而无需关心惰性加载的实现细节,假设有一个获取图表数据的场景: + +```html +<script lang="ts"> + import { QueryClientProvider, QueryClient } from '@tanstack/svelte-query'; + import LazyQuery from './LazyQuery.svelte'; + import CopdGoldGradingChart from './CopdGoldGradingChart.svelte'; + + const queryClient = new QueryClient(); + + // Define the query configuration object, just like with a standard `createQuery`. + const queryOptions = { + queryKey: ['goldGradingData'], + queryFn: async () => { + // Simulate a network request + await new Promise(resolve => setTimeout(resolve, 1500)); + // In a real application, this would be an API call + // const response = await fetch('/api/gold-grading'); + // return await response.json(); + return { data: { totalPatients: 1234, chartPoints: [/* ... */] } }; + } + }; +</script> + +<QueryClientProvider client={queryClient}> + <div style="height: 200vh;"> + <p>Scroll down to see the chart...</p> + </div> + + <LazyQuery {queryOptions}> + {#snippet children({ query })} + {#if query.isLoading} + <p>Loading chart data...</p> + {:else if query.error} + <p>Error: {query.error.message}</p> + {:else if query.data} + <CopdGoldGradingChart + isLoading={query.isFetching} + error={query.error} + refetch={() => query.refetch()} + data={query.data.data} + totalPatients={query.data.data.totalPatients} + /> + {/if} + {/snippet} + </LazyQuery> +</QueryClientProvider> +``` + +- 页面初始加载时,`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 |
