- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
useLocalStorage是 VueUse 中用于将 Vue 响应式状态与浏览器localStorage双向绑定的核心工具,它是useStorage在window.localStorage上的直接封装(见 packages/core/useLocalStorage/index.ts)。本文以 useLocalStorage 官方文档 为主体骨架,深入结合useStorage的源码实现与测试用例,完整讲解其类型签名、序列化机制、跨标签页同步、合并默认值、响应式 key 以及 SSR 注意事项,帮助你彻底掌握这一 Vue 3 状态持久化利器。
一、useLocalStorage 是什么
useLocalStorage创建了一个响应式 ref,可直接读写localStorage,且数据变更自动持久化、存储变化自动反向同步。它解决了手动调用localStorage.setItem/getItem时"状态与存储脱节"的痛点,让持久化状态和普通 Vue 响应式状态一样使用。
官方文档对它的定义非常简洁:
Reactive LocalStorage.
Usage: Please refer to
useStorage.
这意味着它的全部行为、选项与能力均由 useStorage 官方文档 定义。useLocalStorage只是把第三个存储参数固定绑定为localStorage,源码验证了这一设计:
// packages/core/useLocalStorage/index.ts export function useLocalStorage<T extends(string | number | boolean | object | null)>( key: MaybeRefOrGetter<string>, initialValue: MaybeRefOrGetter<T>, options: UseStorageOptions<T> = {}, ): RemovableRef<any> { const { window = defaultWindow } = options return useStorage(key, initialValue, window?.localStorage, options) }核心实现只有 3 行:从options中取出可配置的window(默认取defaultWindow),然后把window?.localStorage作为存储源传给useStorage。所以理解 useLocalStorage 的一切,就是理解 useStorage。
二、基本用法
import { useLocalStorage } from '@vueuse/core' // 绑定对象 const state = useLocalStorage('my-store', { hello: 'hi', greeting: 'Hello' }) // 绑定布尔值,返回 Ref<boolean> const flag = useLocalStorage('my-flag', true) // 绑定数字,返回 Ref<number> const count = useLocalStorage('my-count', 0) // 绑定字符串 const id = useLocalStorage('my-id', 'some-string-id') // 删除存储中的数据(写入 null 会触发 removeItem) state.value = null与useStorage的签名对比如下(useStorage 多了第三参数 storage):
useStorage(key, defaults, storage, options?) // 第三参数是存储源,可传 localStorage / sessionStorage / 自定义 StorageLike useLocalStorage(key, initialValue, options?) // 第三参数固定为 window.localStorage官方演示页(packages/core/useStorage/demo.vue)展示了真实场景:同一 key 创建两个useLocalStorage实例,一个用于v-model编辑,一个用于实时预览序列化结果,两者通过存储事件保持同步:
<script setup lang="ts"> import { useStorage } from '@vueuse/core' const theDefault = { name: 'Banana', color: 'Yellow', size: 'Medium', count: 0, } const state = useStorage('vue-use-local-storage', theDefault) const state2 = useStorage('vue-use-local-storage', theDefault) // 与 state 自动同步 </script> <template> <input v-model="state.name" type="text"> <input v-model="state.color" type="text"> <input v-model="state.size" type="text"> <input v-model.number="state.count" type="range" min="0" step="0.01" max="1000"> </template>三、类型签名与返回值
useLocalStorage依据initialValue的类型提供 5 组重载签名(与 test/snapshots/tsnapi/@vueuse/core/index.snapshot.d.ts 中的公开类型声明一致):
export declare function useLocalStorage( key: MaybeRefOrGetter<string>, initialValue: MaybeRefOrGetter<string>, options?: UseStorageOptions<string>, ): RemovableRef<string> export declare function useLocalStorage( key: MaybeRefOrGetter<string>, initialValue: MaybeRefOrGetter<boolean>, options?: UseStorageOptions<boolean>, ): RemovableRef<boolean> export declare function useLocalStorage( key: MaybeRefOrGetter<string>, initialValue: MaybeRefOrGetter<number>, options?: UseStorageOptions<number>, ): RemovableRef<number> export declare function useLocalStorage<T>( key: MaybeRefOrGetter<string>, initialValue: MaybeRefOrGetter<T>, options?: UseStorageOptions<T>, ): RemovableRef<T> export declare function useLocalStorage<T = unknown>( key: MaybeRefOrGetter<string>, initialValue: MaybeRefOrGetter<null>, options?: UseStorageOptions<T>, ): RemovableRef<T>关键点:
key与initialValue均为MaybeRefOrGetter:可以传普通值、ref,或返回值的 getter 函数;- 返回值是
RemovableRef<T>:比普通Ref<T>多了RemovableRef语义——将.value设为null或undefined时,会从存储中移除该 key(源码write()中v == null分支执行storage.removeItem); initialValue为null时的T = unknown重载:因为无法从null推断数据类型,此时必须配合自定义serializer或显式指定StorageSerializers才能获得完整类型(详见第五节)。
四、Options 完整配置
useLocalStorage的options类型为UseStorageOptions<T>,完整定义在 packages/core/useStorage/index.ts:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
deep | boolean | true | 是否深度监听对象/数组变化,深度修改对象属性也会触发写入 |
listenToStorageChanges | boolean | true | 是否监听storage事件,实现多标签页同步 |
writeDefaults | boolean | true | 存储中不存在该 key 时,是否把默认值写入存储 |
mergeDefaults | boolean \| (storageValue, defaults) => T | false | 是否用默认值合并存储值(对象执行浅合并),可传函数自定义合并逻辑 |
serializer | Serializer<T> | 按类型自动选择 | 自定义序列化函数{ read, write } |
onError | (error: unknown) => void | console.error | 读写出错时的回调 |
shallow | boolean | false | 是否用shallowRef代替ref,适合大数据量、只做整体替换的场景 |
initOnMounted | boolean | false | 是否等到组件 mounted 后再读取存储(SSR 安全场景常用) |
flush | 'pre' \| 'post' \| 'sync' | 'pre' | watch 的触发时机,与 Vue watch 语义一致 |
eventFilter | EventFilter | 无 | 事件过滤器(如debounceFilter(100)做防抖写入) |
window | Window | defaultWindow | 自定义 window 实例(iframe、测试环境) |
官方示例:
useLocalStorage('key', defaults, { // 深度监听对象/数组变化(默认 true) deep: true, // 通过 storage 事件跨标签页同步(默认 true) listenToStorageChanges: true, // 存储中不存在时写入默认值(默认 true) writeDefaults: true, // 使用 shallowRef 代替 ref(默认 false) shallow: false, // 组件 mounted 后才初始化读取(默认 false) initOnMounted: false, // 自定义错误处理(默认 console.error) onError: e => console.error(e), // watch 触发时机(默认 'pre') flush: 'pre', })其中eventFilter的用法在测试中有明确验证(packages/core/useStorage/index.browser.test.ts 的eventFilter用例):配合debounceFilter(100)后,修改值不会立即写入存储,而是等待 100ms 防抖窗口结束后才setItem,适合高频更新场景:
import { debounceFilter } from '@vueuse/shared' const ref = useLocalStorage('key', { name: 'a', data: 123 }, { eventFilter: debounceFilter(100), }) ref.value.name = 'b' // 不会立即写入 // 100ms 后自动写入 {"name":"b","data":123}五、序列化机制与 StorageSerializers
localStorage只能存储字符串。useStorage会根据initialValue的数据类型自动挑选序列化器。类型推断逻辑位于 packages/core/useStorage/guess.ts:
export function guessSerializerType<T>(rawInit: T) { return rawInit == null ? 'any' : rawInit instanceof Set ? 'set' : rawInit instanceof Map ? 'map' : rawInit instanceof Date ? 'date' : typeof rawInit === 'boolean' ? 'boolean' : typeof rawInit === 'string' ? 'string' : typeof rawInit === 'object' ? 'object' : !Number.isNaN(rawInit) ? 'number' : 'any' }官方文档提供的内置序列化器一览(源码实现见StorageSerializers定义):
| 类型 | 说明 | 写入实现 | 读取实现 |
|---|---|---|---|
string | 普通字符串 | String(v) | 原样返回 |
number | 数字 | String(v) | Number.parseFloat(v) |
boolean | 布尔值 | String(v) | v === 'true' |
object | JSON 对象/数组 | JSON.stringify(v) | JSON.parse(v) |
map | JavaScriptMap | JSON.stringify(Array.from(entries)) | new Map(JSON.parse(v)) |
set | JavaScriptSet | JSON.stringify(Array.from(v)) | new Set(JSON.parse(v)) |
date | Date对象 | v.toISOString() | new Date(v) |
any | 原始字符串透传 | String(v) | 原样返回 |
5.1 自定义序列化
当内置序列化器不满足需求(例如存储加密数据、自定义格式)时,传入serializer: { read, write }:
import { useLocalStorage } from '@vueuse/core' useLocalStorage('key', {}, { serializer: { read: (v: any) => v ? JSON.parse(v) : null, write: (v: any) => JSON.stringify(v), }, })5.2 默认值为 null 时必须显式指定序列化器
官方文档特别提示:当initialValue为null时,guessSerializerType只能推断出'any'(原始字符串透传),无法自动处理对象。此时应复用内置序列化器:
import { StorageSerializers, useLocalStorage } from '@vueuse/core' const objectLike = useLocalStorage('key', null, { serializer: StorageSerializers.object }) objectLike.value = { foo: 'bar' } // 以 JSON 格式写入测试用例(packages/core/useStorage/index.browser.test.ts)验证了各种类型的序列化行为:对象写入{"name":"a","data":123}、Map 写入[[1,"a"],[2,2]]、Set 写入[1,"2"]、Date 写入 ISO 字符串,并支持深度修改后自动重新序列化。
六、合并默认值 mergeDefaults
默认行为是:存储中已有值则直接采用存储值,忽略默认值。这可能导致结构缺失——例如客户端存储的是旧版本的{"hello": "hello"},而默认值新增了greeting字段:
localStorage.setItem('my-store', '{"hello": "hello"}') const state = useLocalStorage('my-store', { hello: 'hi', greeting: 'hello' }) console.log(state.value.greeting) // undefined,存储中没有该字段启用mergeDefaults: true后,对象会执行浅合并:存储值覆盖同名属性,默认值补充缺失属性:
localStorage.setItem('my-store', '{"hello": "nihao"}') const state = useLocalStorage( 'my-store', { hello: 'hi', greeting: 'hello' }, { mergeDefaults: true }, ) console.log(state.value.hello) // 'nihao',来自存储 console.log(state.value.greeting) // 'hello',来自合并的默认值对于数组,浅合并直接采用存储数组(测试中useStorage('k', [2], storage, { mergeDefaults: true })在存储为[1]时得到[1])。需要深层合并时可传自定义函数:
const state = useLocalStorage( 'my-store', { hello: 'hi', greeting: 'hello' }, { mergeDefaults: (storageValue, defaults) => deepMerge(defaults, storageValue) }, )测试用例还覆盖了自定义函数的通用性:(value, initial) => value + initial可实现数字累加,(value, initial) => [...initial, ...value]可实现数组合并(见 packages/core/useStorage/index.browser.test.ts 的mergeDefaults option用例)。注意mergeDefaults仅在**首次读取(非事件驱动)**时生效,源码中该逻辑位于read()函数的!event && mergeDefaults分支。
七、跨标签页与同文档同步原理
useLocalStorage默认开启listenToStorageChanges: true,这是它实现"多标签页自动同步"的关键。源码的监听逻辑(packages/core/useStorage/index.ts):
if (window && listenToStorageChanges) { if (storage instanceof Storage) useEventListener(window, 'storage', onStorageEvent, { passive: true }) else useEventListener(window, customStorageEventName, onStorageCustomEvent) }- 标准
Storage(localStorage):监听原生storage事件,实现跨标签页同步。update()中会校验event.storageArea、event.key,只处理属于当前 key 的变更; - 自定义 StorageLike:监听 VueUse 自定义事件
'vueuse-storage'(常量customStorageEventName),实现同一文档内多个实例同步。因为StorageEvent无法用非内置 storageArea 构造,写入时会dispatchEvent一个携带{ key, oldValue, newValue, storageArea }的自定义事件(源码dispatchWriteEvent())。
测试用例验证了两类同步:
// 同文档同步(标准 Storage) const state1 = useStorage(KEY, 0, storage) const state2 = useStorage(KEY, 0, storage) state1.value = 1 await nextTick() expect(state2.value).toBe(1) // 通过自定义事件同步 // 跨标签页(原生 storage 事件) window.dispatchEvent(new StorageEvent('storage', { storageArea: localStorage, key: KEY, newValue: '1' })) expect(data.value).toBe(1)整个读写流程是"写时序列化、读时反序列化、变更双向驱动"的闭环:
ref变化 →watchPausable触发write(newValue)→ 序列化 →setItem/removeItem(null时删除);storage事件/自定义事件到达 →update()→read()反序列化 → 更新ref(期间pauseWatch暂停写回,避免无限循环,事件驱动时用nextTick(resumeWatch)恢复,见源码update()的finally块)。
八、响应式 Key:key 可动态变化
key支持 ref 或 getter,key 变化时自动读取新位置的存储数据:
import { ref } from 'vue' import { useLocalStorage } from '@vueuse/core' const userId = ref('user-1') const userData = useLocalStorage( () => `user-data-${userId.value}`, { name: '' }, ) // 切换用户:自动从 user-data-2 读取数据 userId.value = 'user-2'实现原理:源码中用computed(() => toValue(key))生成keyComputed,并watch(keyComputed, () => update())。测试updates on key change when the new storage value is presented验证了:key 切换到已有数据的另一个 key 时,ref会更新为新存储值;而changes to defaults on key change when the new storage value is undefined验证了新 key 无数据时回落到默认值。测试同时确认:初始 key 位置的数据会保留(writeDefaults: true会把当时的默认值写回原 key),切换不会丢失旧数据。
九、SSR / Nuxt 注意事项
官方文档对 Nuxt 3 有一条重要提示:
在 Nuxt 3 中使用时,该函数不会被自动导入,因为会与 Nitro 内置的
useStorage()冲突。如果希望使用 VueUse 的这个函数,请使用显式导入。
import { useLocalStorage } from '@vueuse/core'此外,服务端渲染环境下window不存在。useLocalStorage的处理方式是:
defaultWindow = isClient ? window : undefined(见 packages/core/_configurable.ts),SSR 时window为undefined,window?.localStorage为undefined;useStorage检测到!storage时,会通过getSSRHandler('getDefaultStorage', () => defaultWindow?.localStorage)获取存储源(见 packages/core/ssr-handlers.ts),允许在服务端注入自定义处理器;- 若最终仍无存储可用,则退化为纯内存 ref(源码
if (!storage) return data),不会抛出异常,但也不具备持久化能力。
需要真正"SSR 安全 + 客户端持久化"时,配合initOnMounted: true让初始化读取推迟到组件挂载之后(测试initOnMounted用例验证了挂载前返回默认值、挂载后读取真实存储值的时序)。
十、常见边界行为速查
state.value = null/undefined:触发removeItem删除该 key,测试remove value用例验证删除后getItem返回 falsy;- 存储值优先:key 已存在时采用存储值(测试
use storage value if present用例逐一验证了字符串、数字、布尔、对象、空字符串、Map、Set 的"存储优先"行为); writeDefaults:key 不存在且默认值非空时,首次初始化即把默认值写入存储;shallow: true:内部使用shallowRef,深度修改对象不会触发写入,只有整体替换.value才同步;- 错误处理:任何读写异常都会进入
onError(默认console.error),不会中断应用;测试handle error用例模拟了setItem抛错与 SSR 处理器抛错两种场景。
十一、源码结构速览
围绕本主题可继续深入阅读的仓库文件:
- packages/core/useLocalStorage/index.ts:
useLocalStorage本体(固定 localStorage 的薄封装) - packages/core/useLocalStorage/index.md:官方文档(本文骨架来源)
- packages/core/useStorage/index.ts:
useStorage完整实现、UseStorageOptions<T>、StorageSerializers - packages/core/useStorage/guess.ts:序列化器类型自动推断
- packages/core/useStorage/index.md:useStorage 完整官方文档
- packages/core/useStorage/index.browser.test.ts:覆盖序列化、合并、同步、事件过滤、SSR 处理器等 30 余个测试用例
- packages/core/useStorage/demo.vue:官方演示组件
- packages/core/ssr-handlers.ts:SSR 存储处理器(
getSSRHandler/setSSRHandler)
总结
useLocalStorage的价值在于把繁琐的存储读写收敛为一行代码:类型安全的响应式 ref、自动序列化、跨标签页同步、动态 key、默认值合并与 SSR 兼容,全部由useStorage的底层实现统一提供。掌握本文覆盖的类型签名、UseStorageOptions各参数、StorageSerializers选择逻辑与同步原理之后,你可以在任何 Vue 3 项目中安全、高效地使用它来持久化用户偏好、草稿、配置等状态数据。若需要会话级存储,可参考同族的useSessionStorage;需要异步存储后端(如 IndexedDB),则参考useStorageAsync。
- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
相关推荐
VueUse useStorage 完全指南:响应式 LocalStorage/SessionStorage 绑定实战与源码解析
VueUse useStorage 完全指南:响应式 LocalStorage/SessionStorage 绑定实战与源码解析 导读 useStorage 是
前端VueUse useGeolocation 完全指南:基于原生 Geolocation API 的响应式定位封装
VueUse useGeolocation 完全指南:基于原生 Geolocation API 的响应式定位封装 useGeolocation 是 VueUse
前端VueUse useLocalStorage 实战指南:让 localStorage 响应式地与 Vue 3 状态同步
VueUse useLocalStorage 实战指南:让 localStorage 响应式地与 Vue 3 状态同步 导读 useLocalStorage 是
前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考