☰
深入理解 VueUse `useStorage`:为 Vue 3 应用打造响应式 Web Storage 状态层
2026/10/11 2:17:04 网站建设 项目流程

深入理解 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布尔值
objectJSON 对象/数组
mapJavaScriptMap
setJavaScriptSet
dateJavaScriptDate(经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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询