- 前端
- 缓存
- 状态管理
【免费下载链接】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.
导读
HydrationBoundary是@tanstack/svelte-query为 Svelte/SvelteKit 应用提供的服务端状态水合(hydration)边界组件:它接收由dehydrate生成的序列化状态,并将其注入 QueryClient 缓存,从而让服务端预取的数据直达客户端、避免重复请求。本文从HydrationBoundary的类型别名入手,结合 组件源码、底层 hydration 实现、单元测试 与 SSR 示例,带你完整掌握"服务端预取 → 序列化传输 → 客户端水合"的整条链路。
一、类型别名:HydrationBoundary 是什么
在 type-aliases/HydrationBoundary.md 中,官方文档给出了它的类型定义:
type HydrationBoundary = SvelteComponent;这个类型别名表明:从类型系统的角度看,HydrationBoundary就是一个标准的 Svelte 组件类型(SvelteComponent),它可以直接在.svelte文件中作为组件标签使用。与此同时,在 variables/HydrationBoundary.md 中,它又被声明为一个LegacyComponentType的常量导出:
const HydrationBoundary: LegacyComponentType;两个声明对应同一导出名的两种视角:作为值使用时它是一个 Svelte 组件(运行时实体),作为类型使用时它是SvelteComponent(类型实体)。该导出最初定义于 Svelte 官方类型声明(node_modules/.../svelte/types/index.d.ts),并由@tanstack/svelte-query重新导出,位于 packages/svelte-query/src/index.ts:32:
export { default as HydrationBoundary } from './HydrationBoundary.svelte'也就是说,包入口直接把.svelte组件文件作为默认导出重新命名后公开,type-aliases与variables两个参考页正是对这个导出的类型侧与值侧注解。
二、组件实现:HydrationBoundary.svelte 做了什么
真正被导出的组件实现位于 packages/svelte-query/src/HydrationBoundary.svelte,整体非常精简:
<script lang="ts"> import { useHydrate } from './useHydrate.js' import type { Snippet } from 'svelte' import type { DehydratedState, HydrateOptions, QueryClient, } from '@tanstack/query-core' type Props = { children: Snippet state: DehydratedState options: HydrateOptions | undefined queryClient: QueryClient | undefined } const { children, state, options = undefined, queryClient = undefined, }: Props = $props() useHydrate(state, options, queryClient) </script> {@render children()}2.1 组件 Props 一览
该组件采用 Svelte 5 的 runes 语法($props()解构),共暴露四个属性:
| Prop | 类型 | 说明 |
|---|---|---|
children | Snippet | 需要在水合边界内渲染的子树,通过{@render children()}输出 |
state | DehydratedState | 由服务端dehydrate()生成的、需注入缓存的序列化状态 |
options | HydrateOptions \| undefined | 控制水合过程的选项,可选 |
queryClient | QueryClient \| undefined | 指定要写入水合状态的客户端;缺省时使用最近上下文中的 QueryClient |
组件挂载后立即调用useHydrate(state, options, queryClient),随后仅负责渲染插槽内容——它是一个"渲染无关"的纯副作用边界组件:不产生任何可见 UI,只负责把水合数据交给缓存。
2.2 与 useHydrate 的关系
组件内部的逻辑全部委托给 useHydrate 函数,其完整实现如下:
export function useHydrate( state?: unknown, options?: HydrateOptions, queryClient?: QueryClient, ) { const client = useQueryClient(queryClient) if (state) { hydrate(client, state, options) } }函数签名与官方文档 functions/useHydrate.md 完全对应:
function useHydrate( state?: unknown, options?: HydrateOptions, queryClient?: QueryClient): void;要点有三:
- 客户端解析:
useQueryClient(queryClient)会优先使用显式传入的queryClient,否则从最近的上下文(QueryClientProvider建立的 context)中获取。useQueryClient的实现见 packages/svelte-query/src/useQueryClient.ts:24。 - 空状态短路:只有
state存在时才调用hydrate,避免空数据时的无意义遍历。 - 仅执行一次:
useHydrate返回void,不返回响应式状态;官方文档明确提示——HydrationBoundary是对useHydrate的封装,只有当你需要在自己的组件内部(而非 JSX/Svelte 模板层)发起水合时才应直接使用useHydrate。
三、底层原理:dehydrate / hydrate 与缓存合并策略
useHydrate调用的hydrate来自@tanstack/query-core,与dehydrate对称实现于 packages/query-core/src/hydration.ts。理解这两个函数,才算真正理解HydrationBoundary的底层行为。
3.1 DehydratedState 的结构
DehydratedState 接口 定义了一个可序列化的缓存快照:
export interface DehydratedState { mutations: Array<DehydratedMutation> queries: Array<DehydratedQuery> }其中每个DehydratedQuery(hydration.ts:87-95)包含queryHash、queryKey、state(QueryState)、dehydratedAt(脱水时间戳)、可选meta、promise(进行中请求的在途 Promise)与queryType(如'infinite')。这份快照默认只包含成功状态的查询与暂停状态的 mutation,可通过DehydrateOptions.shouldDehydrateQuery/shouldDehydrateMutation定制筛选规则。
3.2 hydrate 的合并策略(时间戳优先)
hydrate的核心逻辑(hydration.ts:306-436)决定了水合并非盲目覆盖:
- 缓存中不存在该查询:直接用脱水快照构建查询,并把
fetchStatus重置为'idle',避免新查询卡在 fetching 状态(hydration.ts:381-410)。 - 缓存中已存在该查询:只有当脱水数据的
dataUpdatedAt晚于现有数据的更新时间时才会覆盖;若脱水时查询仍为pending但已有数据,会按dehydratedAt推断其为success状态(hydration.ts:352-380)。这正是官方文档所述"新查询会基于更新时间戳智能合并"的底层来源。 - 在途请求续传:若脱水快照携带了尚未完成的
promise,且目标缓存中没有更新数据,hydrate会调用query.fetch()并把该 Promise 作为initialPromise复用,从而"续传"而非重新请求(hydration.ts:413-433)。
3.3 HydrateOptions 与数据反序列化
HydrateOptions(hydration.ts:68-78)允许在水合时注入默认选项:
export interface HydrateOptions { defaultOptions?: { deserializeData?: TransformerFn queries?: QueryOptions mutations?: MutationOptions<unknown, DefaultError, unknown, unknown> } }deserializeData:反向转换由DehydrateOptions.serializeData施加的变换(例如服务端对非 JSON 可序列化数据做了包装,客户端水合时再还原)。queries/mutations:合并到每个被恢复的查询 / mutation 上的默认选项,优先级高于客户端QueryClient的defaultOptions.hydrate(参见 hydration.ts:386-388)。
四、实战:SvelteKit SSR 中的完整水合流程
HydrationBoundary最常见的落地场景是 SvelteKit 的 SSR:服务端在load函数中预取数据并脱水,客户端用<HydrationBoundary>包裹组件树完成水合。仓库中的 examples/svelte/ssr 提供了可直接运行的最小 SSR 示例。
4.1 服务端:layout load 中创建 QueryClient
examples/svelte/ssr/src/routes/+layout.ts 在每个请求中创建全新的QueryClient,并用 SvelteKit 的browser模块禁用浏览器端自动请求(避免服务器上的查询在 HTML 已发送后仍在服务端异步执行):
import { QueryClient } from '@tanstack/svelte-query' import type { LayoutLoad } from './$types' import { browser } from '$app/environment' export const load: LayoutLoad = () => { const queryClient = new QueryClient({ defaultOptions: { queries: { enabled: browser, staleTime: 60 * 1000, }, }, }) return { queryClient } }该配置方式与 docs/framework/svelte/ssr.md 中推荐的"Setup"方案一致:enabled: browser只影响组件层的createQuery自动执行,不会禁用queryClient.query()(它正是下面服务端预取所使用的 API)。
4.2 服务端:page load 中预取并脱水
页面级 examples/svelte/ssr/src/routes/+page.ts 通过parent()拿到 layout 中的 queryClient,执行预取:
import { noop } from '@tanstack/svelte-query' import type { PageLoad } from './$types' import { api } from '$lib/api' export const load: PageLoad = async ({ parent, fetch }) => { const { queryClient } = await parent() await queryClient .query({ queryKey: ['posts', 10], queryFn: () => api(fetch).getPosts(10), }) .catch(noop) }注意两点:
- 必须使用 SvelteKit 提供的
fetch(来自 load 参数),它才能正确地参与服务端渲染的请求转发; .catch(noop)吞掉异常,避免预取失败导致整个 load 崩溃(noop由@tanstack/svelte-query导出,来源于 query-core 的 utils 同源工具)。
4.3 客户端:HydrationBoundary 水合
examples/svelte/ssr/src/routes/+layout.svelte 用QueryClientProvider把 load 返回的 queryClient 注入上下文,之后组件树内的createQuery直接命中已预热的缓存、不再发起网络请求:
<script lang="ts"> import '../app.css' import { QueryClientProvider } from '@tanstack/svelte-query' import { SvelteQueryDevtools } from '@tanstack/svelte-query-devtools' const { data, children } = $props() </script> <QueryClientProvider client={data.queryClient}> <main> {@render children()} </main> <SvelteQueryDevtools /> </QueryClientProvider>在组件树更深处,则是HydrationBoundary的典型用法(即 functions/useHydrate.md 中给出的官方示例,dehydratedState通常来自服务端 load 中dehydrate(queryClient)的产物,并随页面 HTML 一并传递到浏览器):
<script lang="ts"> import { HydrationBoundary } from '@tanstack/svelte-query' import type { DehydratedState } from '@tanstack/svelte-query' import Posts from './Posts.svelte' let { dehydratedState }: { dehydratedState: DehydratedState } = $props() </script> <HydrationBoundary state={dehydratedState}> <Posts /> </HydrationBoundary>4.4 水合与 refetch 的分工
HydrationBoundary只负责把服务端数据写入缓存,不负责阻止后续刷新。渲染完成后,客户端组件层的createQuery会基于staleTime、dataUpdatedAt等元数据决定是否需要重新拉取——这正是 ssr.md 中对比query方案优于initialData方案的核心原因:水合后的缓存完整保留了服务端抓取时间(dataUpdatedAt),而initialData无法获知抓取时间,导致过期判断以页面加载时刻为基准。
五、测试验证:水合确实把数据写入了缓存
仓库为HydrationBoundary提供了完整的单元测试,位于 packages/svelte-query/tests/HydrationBoundary/HydrationBoundary.svelte.test.ts:
it('should hydrate queries to the cache on context', async () => { const dehydratedState = JSON.parse(stringifiedState) const rendered = render(Base, { props: { queryClient, dehydratedState, queryFn: () => sleep(20).then(() => 'string'), }, }) expect(rendered.getByText('data: stringCached')).toBeInTheDocument() await vi.advanceTimersByTimeAsync(20) expect(rendered.getByText('data: string')).toBeInTheDocument() })测试流程完整复现了生产链路,可作为理解组件行为的最佳"活文档":
- 准备脱水状态(beforeEach,第 11-25 行):在一个临时
QueryClient上执行query()预取(queryKey 为['string'],返回'stringCached'),随后调用dehydrate()得到状态并JSON.stringify序列化,最后clear()清空客户端——模拟"数据只存在于传输载荷中"。 - 渲染被测组件:测试夹具 Base.svelte 先通过
setQueryClientContext(queryClient)注入测试客户端并创建createQuery,再用<HydrationBoundary state={dehydratedState}>包裹,渲染后断言界面立即显示data: stringCached——证明水合数据在首次渲染时已就位,客户端没有发起请求。 - 验证后续刷新:推进 20ms 定时器后,
queryFn的返回值'string'取代水合数据——证明水合不冻结查询,过期后仍会正常 refetch。
测试同时也验证了 Props 的可选性:options={undefined}与queryClient={undefined}时,组件会从最近上下文解析客户端并使用默认水合选项。
六、使用要点与注意事项
state的类型是DehydratedState:应来自服务端dehydrate(queryClient)的返回值,并经过 JSON 序列化传输(如嵌入 HTML);反序列化后再传给组件。- 智能合并而非覆盖:若客户端缓存已存在更新数据,
hydrate会依据dataUpdatedAt时间戳跳过旧数据(hydration.ts:352-380),不会产生"水合回退"式覆盖。 - QueryClient 解析顺序:显式传入的
queryClient优先,否则取最近上下文中的客户端;组件应置于 QueryClientProvider 之内。 useHydrate与组件的取舍:模板场景用<HydrationBoundary>;需要在组件脚本中自行发起水合时(例如自定义数据注入逻辑)才直接调用useHydrate,二者行为等价(useHydrate.ts)。- 与 Svelte 5 runes 的适配:组件使用
$props()与Snippet渲染子内容,迁移自 v5 的旧版 store 写法可参考 migrate-from-v5-to-v6。 - 服务端禁用自动请求:SSR 场景下务必配合
enabled: browser使用,否则查询会在服务端持续异步执行(详见 ssr.md 的 Setup 一节)。
七、小结
HydrationBoundary在类型层面只是一个SvelteComponent别名,但它是 Svelte Query SSR 数据流中承上启下的关键一环:上游是dehydrate产出的序列化缓存快照,下游是useHydrate → hydrate与 QueryClient 缓存的时间戳智能合并,最终让服务端预取数据"零请求"直达客户端,同时保留完整的dataUpdatedAt语义以支撑后续的过期判断与刷新。通过 组件源码、hydration 底层实现、单元测试 与 SSR 示例 四者对照阅读,你就能从"会用"进阶到"懂原理",在自己的 SvelteKit 应用中搭建出高效、可靠的 SSR 数据水合管线。
- 前端
- 缓存
- 状态管理
【免费下载链接】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.
相关推荐
Svelte Query 服务端状态水合:深入理解 useHydrate 与 HydrationBoundary
Svelte Query 服务端状态水合:深入理解 useHydrate 与 HydrationBoundary 导读 本文聚焦 @tanstack/svelt
前端缓存状态管理彻底解决SSR水合难题:TanStack Query HydrationBoundary核心机制
彻底解决SSR水合难题:TanStack Query HydrationBoundary核心机制 你是否在开发SSR应用时遇到过"水合不匹配"警告?页面闪烁、数
前端缓存状态管理TanStack Query(React Query)服务端渲染与水合(SSR & Hydration)实战指南:SSR/SSG 下的预取、dehydrate 与 hydrate 全流程
TanStack Query(React Query)服务端渲染与水合(SSR & Hydration)实战指南:SSR/SSG 下的预取、dehydrate
前端缓存状态管理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考