深入理解 VueUseuseStorage:为 Vue 3 应用打造响应式 Web Storage 状态层
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
导读
useStorage是 VueUse 状态(State)类别中用于把 Vue 响应式ref与浏览器localStorage/sessionStorage双向绑定的核心组合式函数。在 airi 这类横跨 Web、PWA(Capacitor)与 Electron 桌面的多端应用中,它被大量用于持久化设置项,并进一步被封装为带版本校验、手动重置能力的本地存储抽象。读完本文,你将掌握useStorage的完整调用姿势、默认值与序列化机制、全部配置项的含义,以及如何借鉴 airi 仓库中的版本化封装为生产级应用设计可靠的本地持久化方案。
useStorage创建的是一个可以直接读写、自动同步存储介质的响应式引用(reactive ref),默认绑定localStorage,也可通过第三个参数指定sessionStorage或其他StorageLike对象。本文以仓库内 useStorage.md 为骨架,结合 airi 源码中的真实封装与测试展开讲解。
基本用法:一行代码把响应式状态落到浏览器存储
useStorage最直观的价值在于:你无需再手动getItem/setItem+ 监听事件来同步状态,它根据传入默认值的类型自动选择序列化方式,并返回一个带类型的RemovableRef。
import { useStorage } from '@vueuse/core' // 绑定对象:JSON 序列化 const state = useStorage('my-store', { hello: 'hi', greeting: 'Hello' }) // 绑定布尔值:返回 Ref<boolean> const flag = useStorage('my-flag', true) // 绑定数字:返回 Ref<number> const count = useStorage('my-count', 0) // 绑定字符串并指定 sessionStorage:返回 Ref<string> const id = useStorage('my-id', 'some-string-id', sessionStorage) // 删除存储中的数据 state.value = null几点值得注意:
- 删除数据:把
state.value赋值为null会调用存储介质的removeItem,这正是RemovableRef语义的体现。 - 存储介质可替换:第三个参数只要是满足
getItem/setItem/removeItem接口的对象即可,因此useStorage天然支持单元测试中注入内存存储替身——airi 的测试里就是这么做的(见下文)。 - 返回值带类型:不同默认值类型会命中不同的函数重载,返回
Ref<boolean>、Ref<number>、Ref<string>或Ref<T>。
Nuxt 3 使用提示
当在 Nuxt 3 中使用时,该函数不会被自动导入,以免与 Nitro 内置的同名useStorage()冲突。若你确实要使用 VueUse 版本,请显式import { useStorage } from '@vueuse/core'。airi 仓库的所有应用(如 apps/stage-pocket/package.json、apps/stage-tamagotchi/package.json、apps/component-calling/package.json)也都是通过显式声明@vueuse/core依赖(catalog 版本管理)来使用这套 API 的。
默认值合并策略(Merge Defaults):避免新增字段变成undefined
默认情况下,只要存储里已存在该 key 的值,useStorage就会直接使用存储值而忽略默认值。这意味着当你给默认对象新增属性时,老用户存储中没有这个 key 的对应字段,读取结果会是undefined:
import { useStorage } from '@vueuse/core' localStorage.setItem('my-store', '{"hello": "hello"}') const state = useStorage('my-store', { hello: 'hi', greeting: 'hello' }, localStorage) console.log(state.value.greeting) // undefined,因为存储中没有该字段要解决这种"存储值落后于代码默认值"的问题,可以开启mergeDefaults选项:
import { useStorage } from '@vueuse/core' localStorage.setItem('my-store', '{"hello": "nihao"}') const state = useStorage( 'my-store', { hello: 'hi', greeting: 'hello' }, localStorage, { mergeDefaults: true }, // <-- 开启合并 ) console.log(state.value.hello) // 'nihao',来自存储 console.log(state.value.greeting) // 'hello',来自合并进来的默认值合并规则说明:
mergeDefaults: true时对对象执行浅合并(shallow merge):存储中已有的字段以存储值为准,默认值中新增的字段被补入。- 也可以传入自定义合并函数,例如实现深合并:
import { useStorage } from '@vueuse/core' const state = useStorage( 'my-store', { hello: 'hi', greeting: 'hello' }, localStorage, { mergeDefaults: (storageValue, defaults) => deepMerge(defaults, storageValue) }, )仓库实践:版本化合并的思路升级
airi 在 packages/stage-shared/src/composables/use-versioned-local-storage/index.ts 中把"合并默认值"升级为"版本化存储":写入 localStorage 的值统一包装为{ version, data }结构,每次读取时用satisfiesVersionBy回调比较存储版本与当前defaultVersion,不满足版本要求时走onVersionMismatch策略(keep保留旧值或reset重置为默认值),从而以声明方式解决"代码升级后旧数据不兼容"的问题。其测试 use-versioned-local-storage/index.test.ts 用一个MemoryStorage(仅实现getItem/setItem/removeItem的最小StorageLike)验证了写入settings/live2d/auto-blink-enabled时会持久化为{ version: '2.0.0', data: false }的包装结构——这也是useStorage支持自定义存储介质这一设计带来的直接收益。
自定义序列化(Custom Serialization):从 JSON 到 Map / Set / Date
useStorage会根据默认值类型智能挑选序列化器:对象走JSON.stringify/JSON.parse,数字走Number.toString/parseFloat,等等。你也可以完全接管序列化过程:
import { useStorage } from '@vueuse/core' useStorage( 'key', {}, undefined, { serializer: { read: (v: any) => v ? JSON.parse(v) : null, write: (v: any) => JSON.stringify(v), }, }, )需要注意:当默认值为null时,useStorage无法从类型推断序列化方式,此时应显式提供自定义序列化器或复用内置序列化器:
import { StorageSerializers, useStorage } from '@vueuse/core' const objectLike = useStorage('key', null, undefined, { serializer: StorageSerializers.object }) objectLike.value = { foo: 'bar' }内置序列化器一览(StorageSerializers)
StorageSerializers提供了以下开箱即用的序列化器:
| 类型 | 说明 |
|---|---|
string | 普通字符串(原样读写) |
number | 数字(经parseFloat读取) |
boolean | 布尔值 |
object | JSON 对象/数组 |
map | JavaScriptMap |
set | JavaScriptSet |
date | JavaScriptDate(经toISOString写入) |
any | 原始字符串直通 |
例如把Map持久化到存储:
import { StorageSerializers, useStorage } from '@vueuse/core' const myMap = useStorage('my-map', new Map(), undefined, { serializer: StorageSerializers.map, })Options 完整配置项
useStorage的第四个参数接受UseStorageOptions<T>,完整的调用形态与注释如下:
useStorage('key', defaults, storage, { // 深度监听对象/数组内部变化(默认 true) deep: true, // 通过 storage 事件跨标签页同步(默认 true) listenToStorageChanges: true, // 存储中不存在时把默认值写入存储(默认 true) writeDefaults: true, // 使用 shallowRef 而非 ref(默认 false) shallow: false, // 仅在组件挂载后再初始化读取(默认 false) initOnMounted: false, // 自定义错误处理(默认 console.error) onError: e => console.error(e), // watch 刷新时机(默认 'pre') flush: 'pre', })各选项的底层影响:
deep:决定内部watch是否深度跟踪对象/数组的嵌套变化,进而决定嵌套字段修改时是否触发回写。listenToStorageChanges:监听storage事件实现多标签页同步。开启时跨标签页的修改会实时反映到当前页面;airi 的封装中该选项同样被透传(见 use-local-storage-manual-reset/index.ts,其中options?.listenToStorageChanges !== false时才同步存储来源的变更)。writeDefaults:首次访问时若存储中无此 key,把默认值写入存储,避免下次读取时拿到null。shallow:对大型/复杂对象可减少深层响应式开销。initOnMounted:延迟到onMounted后再读取存储,适合 SSR 场景避免在服务端访问window.localStorage。onError:统一接管解析失败、写入异常等错误,便于接入上报。flush:沿用 Vuewatch的 flush 语义('pre'/'post'/'sync'),决定回写时机。
Reactive Key:让存储键本身可响应
存储键可以是ref或 getter 函数,当 key 变化时useStorage会从新的存储位置读取数据:
import { useStorage } from '@vueuse/core' const userId = ref('user-1') const userData = useStorage( () => `user-data-${userId.value}`, { name: '' }, ) // 切换 key 后,将从新的存储位置读取 userId.value = 'user-2'这一特性非常适合"多实例状态各自持久化"的场景,例如按会话、按角色、按账号维度隔离本地数据。
仓库中的组合式实践:useLocalStorageManualReset
airi 在 packages/stage-shared/src/composables/use-local-storage-manual-reset/index.ts 中把useLocalStorage(即固定为localStorage的useStorage便捷封装)与refManualReset组合,构造出"可手动重置的本地存储 ref":
- 用
useLocalStorage<T>(key, value, options)建立持久化层; - 外层用
refManualReset包装,暴露reset()语义; - 通过双向 watch 在"用户写入"与"存储来源变更"之间桥接,并利用
toRaw比较避免同值回写引发的二次 Pinia 变更循环(见源码注释)。
该封装被实际用于 packages/stage-ui/src/stores/settings/general.ts 的 Pinia store 中,持久化settings/language、settings/disable-transitions、settings/websocket/secure-enabled等设置项,并提供了resetState()一键恢复默认值的能力:
const language = useLocalStorageManualReset<string>('settings/language', '') const disableTransitions = useLocalStorageManualReset<boolean>('settings/disable-transitions', true) function resetState() { language.reset() disableTransitions.reset() // ... }这类"ref + storage + 手动重置"的组合,正是useStorage在真实项目中作为基础构件被二次封装、融入 Pinia 状态管理的典型范式。
类型声明与扩展接口
useStorage相关的完整类型声明(来自 useStorage.md)如下,它揭示了自定义序列化器与事件过滤等扩展点:
export interface Serializer<T> { read: (raw: string) => T write: (value: T) => string } export interface SerializerAsync<T> { read: (raw: string) => Awaitable<T> write: (value: T) => Awaitable<string> } export declare const StorageSerializers: Record< "boolean" | "object" | "number" | "any" | "string" | "map" | "set" | "date", Serializer<any> > export declare const customStorageEventName = "vueuse-storage" export interface StorageEventLike { storageArea: StorageLike | null key: StorageEvent["key"] oldValue: StorageEvent["oldValue"] newValue: StorageEvent["newValue"] } export interface UseStorageOptions<T> extends ConfigurableEventFilter, ConfigurableWindow, ConfigurableFlush { deep?: boolean // 深度监听,默认 true listenToStorageChanges?: boolean // 监听 storage 变化,默认 true writeDefaults?: boolean // 写入默认值,默认 true mergeDefaults?: boolean | ((storageValue: T, defaults: T) => T) // 合并默认值,默认 false serializer?: Serializer<T> // 自定义序列化 onError?: (error: unknown) => void // 错误回调,默认 console.error shallow?: boolean // 使用 shallowRef,默认 false initOnMounted?: boolean // 挂载后初始化,默认 false }重载签名覆盖了string/boolean/number/ 泛型T/null五种形态,其中defaults: null时返回RemovableRef<T>,配合显式serializer使用。此外它还继承了ConfigurableEventFilter、ConfigurableWindow、ConfigurableFlush三个可配置接口,分别用于事件过滤(如eventFilter)、窗口对象注入(如 SSR 中的window)与 watch 刷新时机控制,这为在非浏览器环境(如 Node 测试、Electron 主进程)复用该 API 提供了统一入口。
结语
useStorage用极小的 API 面覆盖了 Web Storage 持久化的全部关键诉求:类型化响应式绑定、智能序列化、默认值合并、多标签页同步、可响应 key 与可插拔存储介质。airi 仓库中的 use-versioned-local-storage、use-local-storage-manual-reset 及其配套测试,展示了如何在其之上构建版本兼容与手动重置等生产级能力,可作为你在自己的 Vue 3 项目中设计本地持久化层的直接参考。如果你还需要异步存储(如 IndexedDB、自定义异步后端),可以继续阅读同一 skill 目录下的 useStorageAsync.md 与 useLocalStorage.md、useSessionStorage.md 等姊妹文档。
【免费下载链接】airi💖🧸 Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-sama's altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考