@brmtech/cache
v0.1.3
Published
A lightweight framework-agnostic cache client for browser projects.
Maintainers
Readme
@brmtech/cache
@brmtech/cache 是一个轻量、框架无关的前端缓存客户端。它适合缓存接口响应、配置、字典、语言包等浏览器端数据,支持持久化存储、TTL、stale-while-revalidate、相同 key 请求去重、批量失效、容量清理、订阅通知和多种 storage adapter。
它不绑定 Vue、React、Vite、Next.js 或任何请求库。你只需要提供一个返回 Promise 的 fetcher。
安装
pnpm add @brmtech/cache
# 或者
npm install @brmtech/cache
yarn add @brmtech/cache快速开始
import { createCache, indexedDBAdapter } from '@brmtech/cache'
interface GameList {
list: Array<{ id: string; name: string }>
total: number
}
const cache = createCache({
namespace: 'my-app',
version: '1.0.0',
storage: indexedDBAdapter({ dbName: 'MY_APP_CACHE' }),
defaultPolicy: {
ttl: '30m',
staleTime: '5m',
staleWhileRevalidate: true,
allowStaleOnError: true,
},
})
export function getGameList(page: number) {
const key = cache.key('/api/games', { page, size: 20 }, { scope: 'country:br' })
return cache.wrap<GameList>(
key,
() => fetch(`/api/games?page=${page}&size=20`).then(res => res.json()),
{
tags: ['game'],
scope: 'country:br',
onUpdate: fresh => {
// stale 后台刷新成功且数据发生变化时触发,可以在这里更新页面状态。
renderGameList(fresh)
},
},
)
}这段代码会带来这些行为:
- 首次调用时执行
fetcher,并把结果写入 IndexedDB。 - 后续同一个 key 在 TTL 内会优先返回缓存。
- 数据超过
staleTime但未超过ttl时,默认先返回旧数据,同时后台刷新。 - 后台刷新拿到新数据后,如果内容发生变化,会触发
onUpdate和subscribe。 - 多个相同 key 的并发
wrap()调用只会执行一次fetcher。 fetcher失败时,如果存在旧缓存且allowStaleOnError为true,会返回旧缓存兜底。
公开导出
可导入 API
import {
createCache,
buildCacheKey,
memoryAdapter,
indexedDBAdapter,
localStorageAdapter,
sessionStorageAdapter,
hybridAdapter,
jsonSerializer,
} from '@brmtech/cache'| 导出 | 说明 |
| --- | --- |
| createCache(options?) | 创建缓存客户端,返回 CacheClient。 |
| buildCacheKey(path, params?, options?) | 生成稳定 key 的纯函数,返回 { key, path, namespace?, version?, scope? }。 |
| memoryAdapter(options?) | 内存 adapter,适合测试、SSR 临时缓存、不需要持久化的场景。 |
| indexedDBAdapter(options?) | IndexedDB adapter,浏览器端推荐持久化方案。 |
| localStorageAdapter(options?) | localStorage adapter,适合小体积 fallback。 |
| sessionStorageAdapter(options?) | sessionStorage adapter,适合当前浏览器会话内缓存。 |
| hybridAdapter(adapters, name?) | 组合多个 adapter,读时按顺序查找,写时至少一个成功即成功。 |
| jsonSerializer | 默认 JSON serializer,使用 JSON.stringify 和 JSON.parse。 |
可导入类型
import type {
CacheClient,
CacheOptions,
CachePolicy,
CacheSetOptions,
CacheWrapOptions,
CacheGetOptions,
CacheKeyOptions,
CacheCleanupOptions,
CacheClearOptions,
CacheEntry,
CacheEntryFilter,
CacheStorageAdapter,
CacheSerializer,
CacheStats,
CacheWriteContext,
CacheFetcher,
CacheSubscribeListener,
Duration,
} from '@brmtech/cache'adapter 的 options 类型会出现在函数参数提示里,但当前入口没有把 MemoryAdapterOptions、IndexedDBAdapterOptions、LocalStorageAdapterOptions、SessionStorageAdapterOptions 作为可直接 import 的命名类型导出。需要复用时可以这样写:
type IDBOptions = Parameters<typeof indexedDBAdapter>[0]核心概念
| 概念 | 说明 | 示例 |
| --- | --- | --- |
| key | 缓存唯一标识。短 key 会自动加上当前实例的 namespace/version 前缀。 | home:list |
| namespace | 项目或模块隔离维度。clear()、cleanup() 默认只处理当前 namespace。 | admin、h5 |
| version | 数据版本隔离维度,适合放应用版本或构建 hash。 | 1.0.0 |
| scope | 用户、租户、地区等业务维度。 | user:123、country:br |
| tags | 批量失效分组。一个 entry 可以有多个 tag。 | product、activity |
| ttl | 最大可用时间,超过后视为 expired。null 表示不设置过期时间。 | 30m |
| staleTime | 新鲜时间,超过后视为 stale,但还未 expired。null 表示没有 stale 阶段。 | 5m |
时间线:
updatedAt ------------- staleAt ------------- expiresAt
fresh stale expired| 状态 | get() | getFresh() | wrap() 默认行为 |
| --- | --- | --- | --- |
| fresh | 返回缓存 | 返回缓存 | 返回缓存 |
| stale | 返回缓存 | 返回 null | 返回旧缓存并后台刷新 |
| expired | 返回 null | 返回 null | 执行 fetcher,失败时可用旧缓存兜底 |
注意:如果 wrap() 的 staleWhileRevalidate 为 false,stale 数据在过期前仍会被直接返回,不会自动刷新。想要强制拉取最新数据,请调用 refresh()。
创建缓存实例
import { createCache, indexedDBAdapter } from '@brmtech/cache'
export const cache = createCache({
namespace: 'my-app',
version: '1.0.0',
storage: indexedDBAdapter({
dbName: 'MY_APP_CACHE',
storeName: 'entries',
}),
memory: {
enabled: true,
maxEntries: 100,
},
defaultPolicy: {
ttl: '10m',
staleTime: '2m',
staleWhileRevalidate: true,
allowStaleOnError: true,
maxEntryBytes: 500 * 1024,
},
cleanup: {
maxEntries: 500,
maxBytes: 50 * 1024 * 1024,
},
touchThrottleMs: 5000,
deleteExpiredOnGet: false,
})CacheOptions
| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| namespace | string | 无 | 当前实例的命名空间。短 key 会解析成 namespace::key 或 namespace::version::key。 |
| version | string | 无 | 当前实例的数据版本。短 key 会带上 version。 |
| storage | CacheStorageAdapter | 有 IndexedDB 时用 indexedDBAdapter(),否则用内存 adapter | 主存储层。 |
| memory.enabled | boolean | true | 是否启用内存一级缓存。 |
| memory.maxEntries | number | 100 | 内存一级缓存最多保留多少条,超过后按 LRU 淘汰。 |
| defaultPolicy.ttl | Duration \| null | '5m' | 默认 TTL。 |
| defaultPolicy.staleTime | Duration \| null | null | 默认 stale 时间。 |
| defaultPolicy.staleWhileRevalidate | boolean | true | wrap() 命中 stale 数据时是否后台刷新。 |
| defaultPolicy.allowStaleOnError | boolean | true | fetcher 失败时是否返回旧缓存。 |
| defaultPolicy.maxEntryBytes | number | 无 | 单条缓存最大字节数,超过则跳过写入。 |
| cleanup.maxEntries | number | 无 | cleanup() 后最多保留多少条未过期数据。 |
| cleanup.maxBytes | number | 无 | cleanup() 后最多保留多少字节未过期数据。 |
| cleanup.maxEntryBytes | number | 无 | 全局默认单条大小限制,会参与写入策略合并。 |
| cleanup.interval | Duration | 无 | 类型中保留的字段,当前核心不会自动按 interval 调度 cleanup()。 |
| cleanup.eviction | 'lru' | 无 | 类型中只允许 lru,当前容量清理实际按 LRU 执行。 |
| cleanup.quotaThreshold | number | 无 | 类型中保留的字段,当前核心逻辑没有读取它。 |
| serializer | CacheSerializer | jsonSerializer | 序列化器。默认要求数据可 JSON 序列化。 |
| shouldCache | (value, context) => boolean \| Promise<boolean> | 无 | 写入前判断是否允许缓存,返回 false 会跳过写入。 |
| touchThrottleMs | number | 5000 | 命中缓存后写回 lastAccessedAt/hitCount 的节流间隔。 |
| deleteExpiredOnGet | boolean | false | get() 发现 expired 数据时是否立即删除。默认保留,便于请求失败时兜底。 |
策略合并优先级从低到高是:内置默认策略、cleanup.maxEntryBytes、defaultPolicy、单次调用 options。
时间参数 Duration
Duration 可以是数字,也可以是带单位的字符串。数字表示毫秒。
type Duration = number | `${number}ms` | `${number}s` | `${number}m` | `${number}h` | `${number}d`可用示例:
'500ms'
'10s'
'5m'
'2h'
'7d'
60000字符串格式不合法时会抛出错误或导致写入失败,建议只使用 ms、s、m、h、d。
key 生成
cache.key(path, params?, options?)
生成稳定 key,并在当前实例中记录 path/namespace/version/scope 元信息,方便后续 set() 和 invalidate({ pathPrefix }) 使用。
const key = cache.key('/api/list', { size: 20, page: 1 })
// my-app::1.0.0::/api/list?page=1&size=20签名:
key(
path: string,
params?: Record<string, unknown> | URLSearchParams,
options?: CacheKeyOptions,
): stringCacheKeyOptions:
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| namespace | string | 覆盖当前实例的 namespace。 |
| version | string | 覆盖当前实例的 version。 |
| scope | string | 加入 key 的业务维度。 |
| keepEmptyString | boolean | 是否保留空字符串参数,默认过滤空字符串。 |
规则:
- query 参数按参数名排序,同名参数再按值排序。
null、undefined会被忽略。- 空字符串默认忽略,传
keepEmptyString: true后保留。 - 数组会展开为多个同名 query 参数。
- 参数名和值会使用
encodeURIComponent编码。 - 普通对象不会深度序列化,会走
String(value),不建议把嵌套对象直接放进 params。 path已经带 query 时,新参数会用&追加。
buildCacheKey(path, params?, options?)
纯函数版本,只生成 key,不依赖缓存实例。
import { buildCacheKey } from '@brmtech/cache'
const built = buildCacheKey('/api/list', { page: 1 }, {
namespace: 'my-app',
version: '1.0.0',
})
console.log(built.key)
console.log(built.path)返回值:
{
key: string
path: string
namespace?: string
version?: string
scope?: string
}如果你用 buildCacheKey() 生成 key,再直接 cache.set(built.key, value),当前 cache 实例不会自动记住 built.path。需要依赖 pathPrefix 失效时,请使用 cache.key(),或写入时手动传 path: built.path。
CacheClient API
cache.set(key, value, options?)
写入一条缓存。
const success = await cache.set('config', config, {
ttl: '1h',
tags: ['config'],
metadata: { source: 'bootstrap' },
})set<T = unknown>(key: string, value: T, options?: CacheSetOptions): Promise<boolean>CacheSetOptions:
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| ttl | Duration \| null | 当前 entry 的 TTL。 |
| staleTime | Duration \| null | 当前 entry 的 stale 时间。 |
| maxEntryBytes | number | 当前 entry 最大字节数,超过会跳过写入。 |
| staleWhileRevalidate | boolean | 策略字段,对 wrap() 有意义;单独 set() 不会把这个行为写入 entry。 |
| allowStaleOnError | boolean | 策略字段,对 wrap()、refresh() 的错误兜底有意义。 |
| path | string | 原始资源路径,用于 invalidate({ pathPrefix })。使用 cache.key() 时通常不用手动传。 |
| namespace | string | 覆盖 entry 的 namespace。 |
| version | string | 覆盖 entry 的 version。 |
| scope | string | entry 的业务作用域。 |
| tags | string[] | entry 的标签。 |
| metadata | Record<string, unknown> | 自定义元信息,会存入 entry。 |
返回 true 表示写入成功。返回 false 表示写入被跳过或失败,例如超过 maxEntryBytes、shouldCache 返回 false、序列化失败或 adapter 写入失败。
cache.get(key, options?)
读取未过期缓存。stale 但未 expired 的数据也会返回。
const config = await cache.get<AppConfig>('config')get<T = unknown>(key: string, options?: CacheGetOptions): Promise<T | null>| 参数 | 类型 | 说明 |
| --- | --- | --- |
| allowStale | boolean | 名称是 stale,但当前实现效果是允许返回 expired 缓存。普通 stale 缓存默认就会返回。请谨慎使用。 |
| maxAge | Duration | 只接受 updatedAt 距今不超过该时间的数据。超过后返回 null。 |
await cache.get('config', { maxAge: '10m' })
await cache.get('config', { allowStale: true })cache.getFresh(key, options?)
只读取 fresh 缓存。只要 entry 已经 stale 或 expired,就返回 null。
const cached = await cache.getFresh<HomeData>('home:list')getFresh<T = unknown>(key: string, options?: CacheGetOptions): Promise<T | null>getFresh() 中只有 maxAge 有实际意义。即使传 allowStale: true,也不会返回 stale 数据。
cache.wrap(key, fetcher, options?)
请求缓存主 API。它会先查缓存,必要时执行 fetcher,成功后写入缓存。
const data = await cache.wrap(
'home:list',
() => fetch('/api/home').then(res => res.json()),
{ ttl: '30m', staleTime: '5m', tags: ['home'] },
)wrap<T = unknown>(
key: string,
fetcher: () => Promise<T>,
options?: CacheWrapOptions<T>,
): Promise<T>CacheWrapOptions<T> 继承 CacheSetOptions,额外支持:
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| onUpdate | (value: T) => void | stale 后台刷新成功且数据变化时触发。初次请求、前台刷新和 refresh() 请直接使用返回值。 |
| isEqual | (cached: T, fresh: T) => boolean | 判断 stale 后台刷新得到的新值是否和旧值等价。等价时只续期缓存,不触发 onUpdate 或 subscribe。默认用 serializer 序列化后比较。 |
执行流程:
| 场景 | 行为 |
| --- | --- |
| fresh 命中 | 直接返回缓存,不执行 fetcher。 |
| stale 命中且 staleWhileRevalidate: true | 返回旧缓存,同时后台执行 fetcher 并写入新结果。 |
| stale 命中且 staleWhileRevalidate: false | 返回旧缓存,不自动刷新。 |
| miss 或 expired | 执行 fetcher,成功后写入并返回新数据。 |
| 同 key 并发请求 | 复用同一个进行中的 fetcher Promise。 |
| fetcher 失败且有旧缓存、allowStaleOnError: true | 返回旧缓存,包括 expired 旧缓存。 |
| fetcher 失败且不允许旧缓存兜底 | 抛出 fetcher 的错误。 |
SWR 更新页面示例:
const data = await cache.wrap(
'home:list',
() => fetch('/api/home').then(res => res.json()),
{
ttl: '30m',
staleTime: '5m',
onUpdate: fresh => setHomeData(fresh),
},
)
setHomeData(data)自定义等价判断:
await cache.wrap(
'home:list',
() => fetch('/api/home').then(res => res.json()),
{
staleTime: '5m',
isEqual: (cached, fresh) => cached.version === fresh.version,
onUpdate: fresh => setHomeData(fresh),
},
)cache.refresh(key, fetcher, options?)
跳过缓存读取,直接执行 fetcher,并把结果写入缓存。
const fresh = await cache.refresh(
'home:list',
() => fetch('/api/home').then(res => res.json()),
{ ttl: '30m', tags: ['home'] },
)refresh<T = unknown>(key: string, fetcher: () => Promise<T>, options?: CacheSetOptions): Promise<T>refresh() 适合用户手动刷新、操作成功后重新拉取最新数据。需要注意,refresh() 在 fetcher 失败时也会读取旧缓存作为 fallback;如果不希望这样,传 { allowStaleOnError: false }。
cache.delete(key)
删除单条缓存。
await cache.delete('config')短 key 会按当前实例的 namespace/version 解析。传入 cache.key() 生成的完整 key 时,会直接删除对应 entry。
cache.invalidate(filter)
按条件批量删除缓存,返回删除条数。
await cache.invalidate({ tags: ['activity'] })
await cache.invalidate({ scope: `user:${userId}` })
await cache.invalidate({ pathPrefix: '/api/activity' })
await cache.invalidate({ keyPrefix: 'home:' })
await cache.invalidate({ keys: ['config', 'home:list'] })invalidate(filter: CacheEntryFilter): Promise<number>CacheEntryFilter:
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| keys | string[] | 指定 key 删除。短 key 会按当前 namespace/version 解析。 |
| keyPrefix | string | 按 key 前缀删除。短前缀会按当前 namespace/version 解析。 |
| pathPrefix | string | 按 entry.path 前缀删除,推荐配合 cache.key() 使用。 |
| tags | string[] | entry 包含任意一个指定 tag 即命中。 |
| namespace | string | 指定 namespace。默认会使用当前实例 namespace。 |
| scope | string | 指定业务作用域。 |
| version | string | 指定版本。 |
| expired | boolean | true 只匹配已过期,false 只匹配未过期。 |
多个条件之间是 AND 关系。例如 { tags: ['product'], scope: 'user:1' } 只会删除同时满足 tag 和 scope 的 entry。
cache.clear(options?)
清理当前实例 namespace 下的全部缓存。
await cache.clear()
await cache.clear({ all: true })clear(options?: { all?: boolean }): Promise<void>如果创建实例时没有配置 namespace,cache.clear() 本身就会清理整个 storage。多应用共享同一个 IndexedDB、localStorage prefix 或 adapter 时,建议务必配置 namespace。
cache.cleanup()
执行过期和容量清理,返回删除条数。
const removed = await cache.cleanup()清理范围默认是当前 namespace。没有配置 namespace 时会扫描整个 storage。
清理顺序:
- 删除 expired 数据。
- 如果超过
cleanup.maxEntries,按 LRU 删除最久未访问的数据。 - 如果超过
cleanup.maxBytes,继续按 LRU 删除,直到总大小达标。
cleanup() 不会自动定时执行。通常可以在应用启动后低频调用:
void cache.cleanup()cache.subscribe(key, listener)
监听某个 key 的写入通知,返回取消订阅函数。
const unsubscribe = cache.subscribe<HomeData>('home:list', (value, entry) => {
render(value)
console.log(entry.updatedAt)
})
unsubscribe()subscribe<T = unknown>(
key: string,
listener: (value: T, entry: CacheEntry<T>) => void,
): () => void订阅只在当前 CacheClient 实例的 JS 运行上下文内生效,不会跨标签页、跨 worker 或跨另一个 cache 实例同步。
cache.getStats()
获取当前实例统计信息。
const stats = cache.getStats()interface CacheStats {
hits: number
misses: number
staleHits: number
writes: number
skips: number
evictions: number
errors: number
}Storage Adapter
选择建议
| Adapter | 持久化 | 适合场景 |
| --- | --- | --- |
| indexedDBAdapter | 是 | 浏览器端推荐方案,适合接口列表、大 JSON、字典、语言包等。 |
| memoryAdapter | 否 | 测试、SSR 临时缓存、不需要持久化的数据。 |
| localStorageAdapter | 是 | 小体积 fallback,不适合大 JSON。 |
| sessionStorageAdapter | 会话内 | 只需要当前浏览器会话保留的数据。 |
| hybridAdapter | 取决于子 adapter | 多存储降级兜底。 |
indexedDBAdapter(options?)
const storage = indexedDBAdapter({
name: 'idb-cache',
dbName: 'MY_APP_CACHE',
storeName: 'entries',
version: 1,
})| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| name | string | 'indexed-db' | adapter 名称。 |
| dbName | string | 'BRM_CACHE' | IndexedDB 数据库名。 |
| storeName | string | 'entries' | object store 名。 |
| version | number | 1 | IndexedDB 数据库版本。 |
| indexedDB | IDBFactory | globalThis.indexedDB | 自定义 IndexedDB 实现,测试中可传 fake-indexeddb。 |
该 adapter 会创建 keyPath 为 key 的 object store,并创建 namespace、scope、version、path、updatedAt、expiresAt、lastAccessedAt 索引。当前筛选逻辑会遍历 entry 后在 JS 中匹配 filter。
memoryAdapter(options?)
const storage = memoryAdapter({
name: 'memory-cache',
maxEntries: 200,
})| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| name | string | 'memory' | adapter 名称。 |
| maxEntries | number | 无 | 最大条数,超过后按 lastAccessedAt LRU 淘汰。 |
localStorageAdapter(options?)
const storage = localStorageAdapter({
prefix: 'my-app:',
maxEntryBytes: 100 * 1024,
})| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| name | string | 'local-storage' | adapter 名称。 |
| prefix | string | '@brm/cache:' | 写入 localStorage 的 key 前缀。 |
| storage | Storage \| null | 当前环境的 localStorage | 自定义 Storage,实现测试或特殊 WebView 适配。 |
| maxEntryBytes | number | 无 | 单条 entry JSON 字符串最大长度,超过会抛错。当前实现按字符串长度判断。 |
sessionStorageAdapter(options?)
参数和 localStorageAdapter 相同,只是默认使用 sessionStorage。
const storage = sessionStorageAdapter({
prefix: 'my-app-session:',
})hybridAdapter(adapters, name?)
const storage = hybridAdapter([
indexedDBAdapter(),
localStorageAdapter(),
memoryAdapter(),
])行为:
get()按数组顺序读取,返回第一个命中的 entry。set()、delete()、clear()会写入所有子 adapter,至少一个成功就算成功。keys()会合并所有可用 adapter 的 key 并去重。entries()会合并所有支持entries()的 adapter,并按entry.key去重。- 传空数组会抛出错误。
- 当前不会把低优先级 adapter 命中的数据自动回填到高优先级 adapter。
自定义 adapter
自定义存储只需要实现 CacheStorageAdapter。
import type { CacheEntry, CacheEntryFilter, CacheStorageAdapter } from '@brmtech/cache'
export function customAdapter(): CacheStorageAdapter {
return {
name: 'custom',
async get<T>(key: string): Promise<CacheEntry<T> | null> {
return null
},
async set<T>(key: string, entry: CacheEntry<T>): Promise<void> {
// write entry
},
async delete(key: string): Promise<void> {
// delete entry
},
async keys(filter?: CacheEntryFilter): Promise<string[]> {
return []
},
async clear(filter?: CacheEntryFilter): Promise<void> {
// clear entries
},
}
}可选方法:
| 方法 | 说明 |
| --- | --- |
| entries(filter?) | 返回完整 entry 列表。实现后 cleanup() 可以少一次批量 get。 |
| getMany(keys) | 批量读取。当前核心保留该接口。 |
| deleteMany(keys) | 批量删除。invalidate() 会优先使用它。 |
序列化
默认 jsonSerializer:
export const jsonSerializer = {
serialize: value => JSON.stringify(value),
deserialize: raw => JSON.parse(raw),
}默认适合缓存普通 JSON 数据。Date、Map、Set、类实例、函数、循环引用等不会被自动恢复原型或可能无法序列化。需要特殊类型时,传入自定义 serializer。
import type { CacheSerializer } from '@brmtech/cache'
const serializer: CacheSerializer = {
serialize(value) {
return JSON.stringify(value)
},
deserialize(raw) {
return JSON.parse(raw)
},
}
const cache = createCache({ serializer })shouldCache 写入过滤
shouldCache 会在写入前执行,可以用于跳过敏感数据、空数据或超出业务规则的数据。
const cache = createCache({
shouldCache(value, context) {
if (context.tags?.includes('sensitive')) return false
if (context.key.includes('token') || context.key.includes('password')) return false
return value != null
},
})CacheWriteContext:
interface CacheWriteContext {
key: string
path?: string
namespace?: string
version?: string
scope?: string
tags?: string[]
size: number
}常见用法
接口缓存
const key = cache.key('/api/products', { page: 1, size: 20 })
const products = await cache.wrap(
key,
() => fetch('/api/products?page=1&size=20').then(res => res.json()),
{
ttl: '10m',
staleTime: '1m',
tags: ['product'],
},
)数据更新后失效相关缓存
await updateProduct(productId, payload)
await cache.invalidate({ tags: ['product'] })用户退出后清理用户维度缓存
await cache.invalidate({ scope: `user:${userId}` })Vue 或 Pinia 中同步 SWR 结果
不建议把 Pinia 本身当作持久化存储 adapter。更推荐用 IndexedDB/localStorage 做缓存持久化,再用 onUpdate 或 subscribe 把刷新后的数据同步到 store。
const data = await cache.wrap(
cache.key('/api/home'),
() => api.getHome(),
{
ttl: '30m',
staleTime: '5m',
onUpdate: fresh => homeStore.setHomeData(fresh),
},
)
homeStore.setHomeData(data)完整类型参考
下面是入口明确导出的类型,按源码声明展开。adapter options 字段请参考上面的 adapter 小节。
type Duration = number | `${number}ms` | `${number}s` | `${number}m` | `${number}h` | `${number}d`
interface CacheEntry<T = unknown> {
key: string
path?: string
value: T
createdAt: number
updatedAt: number
expiresAt: number | null
staleAt: number | null
lastAccessedAt: number
hitCount: number
size: number
version?: string
namespace?: string
scope?: string
tags?: string[]
metadata?: Record<string, unknown>
}
interface CacheEntryFilter {
keys?: string[]
keyPrefix?: string
pathPrefix?: string
tags?: string[]
namespace?: string
scope?: string
version?: string
expired?: boolean
}
interface CacheStorageAdapter {
name: string
get<T>(key: string): Promise<CacheEntry<T> | null>
set<T>(key: string, entry: CacheEntry<T>): Promise<void>
delete(key: string): Promise<void>
keys(filter?: CacheEntryFilter): Promise<string[]>
entries?<T = unknown>(filter?: CacheEntryFilter): Promise<Array<CacheEntry<T>>>
clear(filter?: CacheEntryFilter): Promise<void>
getMany?<T>(keys: string[]): Promise<Array<CacheEntry<T> | null>>
deleteMany?(keys: string[]): Promise<void>
}
interface CacheSerializer {
serialize(value: unknown): string
deserialize<T = unknown>(raw: string): T
}
interface CachePolicy {
ttl?: Duration | null
staleTime?: Duration | null
staleWhileRevalidate?: boolean
allowStaleOnError?: boolean
maxEntryBytes?: number
}
interface CacheSetOptions extends CachePolicy {
path?: string
namespace?: string
version?: string
scope?: string
tags?: string[]
metadata?: Record<string, unknown>
}
interface CacheWrapOptions<T = unknown> extends CacheSetOptions {
isEqual?: (cached: T, fresh: T) => boolean
onUpdate?: (value: T) => void
}
interface CacheGetOptions {
allowStale?: boolean
maxAge?: Duration
}
interface CacheKeyOptions {
namespace?: string
version?: string
scope?: string
keepEmptyString?: boolean
}
interface CacheCleanupOptions {
maxEntries?: number
maxBytes?: number
maxEntryBytes?: number
interval?: Duration
eviction?: 'lru'
quotaThreshold?: number
}
interface CacheOptions {
namespace?: string
version?: string
storage?: CacheStorageAdapter
memory?: {
enabled?: boolean
maxEntries?: number
}
defaultPolicy?: CachePolicy
cleanup?: CacheCleanupOptions
serializer?: CacheSerializer
shouldCache?: (value: unknown, context: CacheWriteContext) => boolean | Promise<boolean>
touchThrottleMs?: number
deleteExpiredOnGet?: boolean
}
interface CacheWriteContext {
key: string
path?: string
namespace?: string
version?: string
scope?: string
tags?: string[]
size: number
}
interface CacheStats {
hits: number
misses: number
staleHits: number
writes: number
skips: number
evictions: number
errors: number
}
type CacheFetcher<T> = () => Promise<T>
type CacheSubscribeListener<T = unknown> = (value: T, entry: CacheEntry<T>) => void
interface CacheClearOptions {
all?: boolean
}
interface CacheClient {
get<T = unknown>(key: string, options?: CacheGetOptions): Promise<T | null>
getFresh<T = unknown>(key: string, options?: CacheGetOptions): Promise<T | null>
set<T = unknown>(key: string, value: T, options?: CacheSetOptions): Promise<boolean>
delete(key: string): Promise<void>
clear(options?: CacheClearOptions): Promise<void>
wrap<T = unknown>(key: string, fetcher: CacheFetcher<T>, options?: CacheWrapOptions<T>): Promise<T>
refresh<T = unknown>(key: string, fetcher: CacheFetcher<T>, options?: CacheSetOptions): Promise<T>
invalidate(filter: CacheEntryFilter): Promise<number>
cleanup(): Promise<number>
subscribe<T = unknown>(key: string, listener: CacheSubscribeListener<T>): () => void
getStats(): CacheStats
key(path: string, params?: Record<string, unknown> | URLSearchParams, options?: CacheKeyOptions): string
}注意事项
- 不要缓存 token、密码、证件号、银行卡号等敏感数据。
- 默认 serializer 只适合普通 JSON 数据。
pathPrefix依赖 entry 的path字段,接口缓存推荐用cache.key()。- IndexedDB 在隐私模式、旧浏览器或特殊 WebView 中可能不可用,可用
hybridAdapter降级。 - localStorage 容量小且同步阻塞,不建议存大 JSON。
- 当前没有内置跨标签页同步,需要时可在业务层接入
BroadcastChannel后调用invalidate()或refresh()。 - 当前不会自动定时执行
cleanup(),需要在应用启动、空闲时机或业务调度中手动调用。 - 没有配置
namespace时,clear()和cleanup()会作用于整个 storage,多个应用共用存储时请谨慎。
本地开发
pnpm install
pnpm run typecheck
pnpm test
pnpm run build构建产物输出到 dist/,包含 ESM、CJS 和类型声明。
