TanStack Query 之 createSyncStoragePersister 实战指南(Preact Query 离线缓存持久化)
【免费下载链接】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
导读
createSyncStoragePersister是 TanStack Query 生态中用于将 Preact Query 的查询缓存同步写入浏览器localStorage/sessionStorage的持久化工具函数。本文围绕 Preact 框架页对应文档createSyncStoragePersister的完整内容展开,覆盖该插件的废弃状态与替代方案、安装方式、与persistQueryClient的组合使用、写入失败重试策略(含removeOldestQuery预设策略)以及全部可配置项与序列化压缩扩展,并结合作者所在仓库query-sync-storage-persister包的真实实现与测试,帮助你掌握在 Preact 应用中落地「离线缓存、刷新不丢数据」的完整方案。
说明:当前仓库中 Preact 框架的插件文档采用
ref+replace的共享文档机制,即 Preact 页面与 React 源页面共享同一份正文,仅将包名由react-query替换为preact-query。本文所写的正文即该机制下 Preact 页面实际渲染的内容,代码示例中的导入路径均为 Preact 版本。
现状:该插件已被标记为 Deprecated
在写作本文时,createSyncStoragePersister已经被标记为deprecated(废弃),并计划在下一个大版本中移除:
This plugin is deprecated and will be removed in the next major version. You can simply use
@tanstack/query-async-storage-persisterinstead.
推荐的新方案是改用异步存储持久化器createAsyncStoragePersister(对应文档见 Preact 框架页 createAsyncStoragePersister),它基于异步存储接口实现,天然适合 IndexedDB 等不阻塞 UI 的存储介质。该废弃标记同样体现在实现源码中——仓库内 query-sync-storage-persister 的 index.ts 对函数注释明确写着:
/** * @deprecated use `createAsyncStoragePersister` from `@tanstack/query-async-storage-persister` instead. */ export function createSyncStoragePersister(/* ... */)尽管如此,由于同步存储简单直接、无需引入异步适配层,它仍然是大量现有项目采用的方式;对于想要理解同步持久化实现原理、或维护存量代码的开发者,这份文档依然值得完整阅读。
安装
该工具以独立包的形式发布,通过@tanstack/query-sync-storage-persister导入。持久化编排函数persistQueryClient与配套的PersistQueryClientProvider则在 Preact 的持久化客户端包@tanstack/preact-query-persist-client中(该包仅是对@tanstack/query-persist-client-core与 Provider 组件的再导出,见 preact-query-persist-client/src/index.ts)。
四种主流包管理器任选其一:
npm install @tanstack/query-sync-storage-persister @tanstack/preact-query-persist-clientpnpm add @tanstack/query-sync-storage-persister @tanstack/preact-query-persist-clientyarn add @tanstack/query-sync-storage-persister @tanstack/preact-query-persist-clientbun add @tanstack/query-sync-storage-persister @tanstack/preact-query-persist-client注意:query-sync-storage-persister是框架无关的核心包(它只依赖@tanstack/query-core与@tanstack/query-persist-client-core),因此无论使用 React Query、Preact Query、Solid Query 还是 Vue Query,创建同步持久化器的方式都完全一致;区别仅在persistQueryClient的导入来源,Preact 场景下必须从@tanstack/preact-query-persist-client导入。
基本用法
createSyncStoragePersister的使用分为三步:
- 导入
createSyncStoragePersister函数; - 创建一个
syncStoragePersister实例; - 把它交给
persistQueryClient(详细编排文档见 Preact 框架页 persistQueryClient)。
结合 Preact Query 的完整示例如下:
import { QueryClient } from '@tanstack/preact-query' import { persistQueryClient } from '@tanstack/preact-query-persist-client' import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' const queryClient = new QueryClient({ defaultOptions: { queries: { // 缓存保留 24 小时(配合持久化,让数据跨越刷新/会话存活) gcTime: 1000 * 60 * 60 * 24, // 24 hours }, }, }) // 写入 window.localStorage const localStoragePersister = createSyncStoragePersister({ storage: window.localStorage, }) // 若想仅保留在当前标签页会话内,可使用 sessionStorage: // const sessionStoragePersister = createSyncStoragePersister({ storage: window.sessionStorage }) persistQueryClient({ queryClient, persister: localStoragePersister, })要点解读:
gcTime(v5 之前称为cacheTime)决定查询数据在内存与存储中的存活时间,持久化场景下通常要设置得比默认值更长,例如 24 小时或Infinity,否则数据很快被清理、持久化失去意义;- 一个持久化器只绑定一个
storage与一个key,如需对多组查询做不同保留策略,可分别创建实例; storage只要符合getItem/setItem/removeItem三个方法的同步存储契约即可,localStorage、sessionStorage都满足。从实现看(query-sync-storage-persister/src/index.ts),若传入的storage为空(undefined/null,例如服务端渲染场景,或 Android WebView 将window.localStorage置为null的配置),返回的 persister 三个方法(persistClient、restoreClient、removeClient)都会退化为noop空操作,保证 SSR 或受限环境下不抛错、不写数据。
持久化失败的重试(Retries)
持久化写入并非总能成功,典型场景是数据体积超过存储配额(如localStorage通常只有几 MB 上限)。此时storage.setItem会抛错。通过给持久化器提供retry函数,可以优雅地处理这类错误。
retry函数接收它尝试保存的persistedClient、本次error以及累计errorCount三个入参,并且必须返回一个新的PersistedClient用于再次尝试持久化;如果返回undefined,则表示不再进行下一次尝试。
其完整类型契约(导出自@tanstack/query-persist-client-core,并经 Preact 持久化包再导出)为:
export type PersistRetryer = (props: { persistedClient: PersistedClient error: Error errorCount: number }) => PersistedClient | undefined对照实现源码(query-sync-storage-persister/src/index.ts)可以看到重试逻辑是一个while循环:先trySave一次,失败后进入循环,每次累加errorCount、调用retry得到新的客户端对象,若存在则继续trySave,直到保存成功或retry返回undefined:
const trySave = (persistedClient: PersistedClient): Error | undefined => { try { storage.setItem(key, serialize(persistedClient)) return } catch (error) { return error as Error } } // persistClient 内部(节流包裹后): // let client = persistedClient // let error = trySave(client) // let errorCount = 0 // while (error && client) { // errorCount++ // client = retry?.({ persistedClient: client, error, errorCount }) // if (client) { // error = trySave(client) // } // }默认行为与预设策略
- 默认不重试:未提供
retry时,写入失败即静默放弃(error存在但client在首次retry?.()返回undefined后退出循环),不会反复尝试。 - 内置的预设策略
removeOldestQuery可从@tanstack/preact-query-persist-client导入。它会在持久化失败时,返回一个移除了最旧查询的新PersistedClient,以此逐次缩小数据体积,直到能成功写入或清空为止——非常适合处理存储空间不足。
import { removeOldestQuery } from '@tanstack/preact-query-persist-client' import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' const localStoragePersister = createSyncStoragePersister({ storage: window.localStorage, retry: removeOldestQuery, })该策略的实际用法可以在仓库测试中看到:query-sync-storage-persister/src/tests/storageIsFull.test.ts 中的「storage full」测试用例在throttleTime: 0的同时配置retry: removeOldestQuery,用来验证存储写满时通过逐条淘汰最旧查询最终成功完成持久化的链路。
API 参考
createSyncStoragePersister
调用该函数创建一个可与persistQueryClient配合使用的syncStoragePersister:
createSyncStoragePersister(options: CreateSyncStoragePersisterOptions)Options
interface CreateSyncStoragePersisterOptions { /** 用于设置与读取缓存项的存储客户端(window.localStorage 或 window.sessionStorage) */ storage: Storage | undefined | null /** 存储缓存时使用的 key */ key?: string /** 为避免频繁写入, * 传入毫秒数以节流(throttle)保存缓存到存储的操作 */ throttleTime?: number /** 如何将数据序列化后写入存储 */ serialize?: (client: PersistedClient) => string /** 如何将存储中的字符串反序列化为数据 */ deserialize?: (cachedString: string) => PersistedClient /** 写入失败时的重试策略 **/ retry?: PersistRetryer }默认值
以下默认值在文档与实现源码(query-sync-storage-persister/src/index.ts)中完全一致:
{ key = `REACT_QUERY_OFFLINE_CACHE`, throttleTime = 1000, serialize = JSON.stringify, deserialize = JSON.parse, }对各默认参数补充说明:
| 参数 | 默认值 | 说明 |
|---|---|---|
key | REACT_QUERY_OFFLINE_CACHE | 存储键名。作为框架无关的核心包,即便在 Preact Query 中使用,默认键名也保持历史命名不变;同一域名下不同应用需显式传不同key避免互相覆盖 |
throttleTime | 1000 | 节流间隔(毫秒)。实现中通过@tanstack/query-core的timeoutManager做节流:间隔内的多次更新只保留最后一次在间隔结束后落盘,避免高频查询更新打爆存储 I/O |
serialize | JSON.stringify | 持久化前把PersistedClient转成字符串 |
deserialize | JSON.parse | 恢复时把字符串还原为PersistedClient |
另外需要注意Storage接口的最小契约是三个同步方法(与 DOM 标准一致):
interface Storage { getItem: (key: string) => string | null setItem: (key: string, value: string) => void removeItem: (key: string) => void }serialize与deserialize的扩展用法
localStorage存在容量上限(通常约 5 MB,各浏览器实现略有差异)。如果确实需要向localStorage写入更多数据,可以覆写serialize/deserialize,借助压缩库(例如 lz-string)对数据先压缩再存储、读取时再解压,从而等效扩大可存储的查询数据量。
以 lz-string 为例的完整 Preact 用法:
import { QueryClient } from '@tanstack/preact-query' import { persistQueryClient } from '@tanstack/preact-query-persist-client' import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' import { compress, decompress } from 'lz-string' const queryClient = new QueryClient({ // staleTime: Infinity 配合持久化,可让数据视为永不过期,优先读缓存 defaultOptions: { queries: { staleTime: Infinity } }, }) persistQueryClient({ queryClient: queryClient, persister: createSyncStoragePersister({ storage: window.localStorage, // 序列化:JSON 字符串先压缩再写入 serialize: (data) => compress(JSON.stringify(data)), // 反序列化:读出的字符串先解压再 JSON.parse deserialize: (data) => JSON.parse(decompress(data)), }), // 持久化缓存的最大存活时长(这里配合 staleTime: Infinity 设为无限大) maxAge: Infinity, })这里同时展示了persistQueryClient侧的maxAge选项:它控制恢复出的缓存最大可保留多久(从存储时间戳起算),默认与QueryClient的gcTime对齐;设置为Infinity表示不做时间淘汰。
底层执行流程一览
将文档行为与实现源码对照,一次完整的「查询更新 → 持久化」执行链路如下:
persistQueryClient订阅 QueryClient 的查询状态变化(详细机制见 persistQueryClient 文档);- 状态变化时,把整个缓存序列化得到
PersistedClient,调用 persister 的persistClient; - 该调用被
throttle(throttleTime)节流,节流窗口内只保留最后一次要保存的数据; - 窗口结束后真正执行
storage.setItem(key, serialize(persistedClient)); - 若抛错则按上文重试循环调用
retry(如removeOldestQuery)逐次缩小体积重试; - 页面刷新/重新打开时,
persistQueryClient通过restoreClient(内部执行deserialize(storage.getItem(key)))恢复缓存,必要时再触发重新验证(refetch),最终实现「离线可读、刷新不丢」的效果。
值得强调的是节流(throttle)与防抖(debounce)的区别:实现采用的是节流,即保证间隔期内至少执行一次、且最后一次待存数据被保留(见 index.ts 内throttle工具函数)。配合storage为空时的 noop 降级,整套设计保证了在同步存储不可用、写入失败、体积超限等边界条件下,持久化层都不会让主流程崩溃。
小结
createSyncStoragePersister让 Preact Query 缓存可以同步落盘到localStorage/sessionStorage,配合persistQueryClient即可实现离线缓存与跨刷新持久化;- 它已进入废弃流程,新项目建议评估使用 createAsyncStoragePersister(异步、适合 IndexedDB),存量代码仍可依据本文保持维护;
- 记得显式设置足够大的
gcTime与key,理解throttleTime、serialize/deserialize默认值及其扩展方式,并在存储可能写满的场景配置retry: removeOldestQuery; - 实现细节(节流、重试循环、noop 降级、默认键名)均可在仓库 packages/query-sync-storage-persister/src/index.ts 及其测试目录(如 storageIsFull.test.ts)中直接查证。
如果想要了解更通用的离线机制与存储恢复流程,可继续阅读 persistQueryClient(Preact) 以及 Preact 框架安装与使用文档。
【免费下载链接】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),仅供参考