airi 项目实践:用 VueUse useLocalStorage 实现响应式本地持久化状态
发布时间:2026/9/11 14:42:19 作者:尧图编辑部 阅读量:1,286

airi 项目实践用 VueUse useLocalStorage 实现响应式本地持久化状态【免费下载链接】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-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi本篇技术指南围绕 VueUse 的useLocalStorage组合式函数展开它在 airi自托管 AI 伴侣应用覆盖 Web / macOS / Windows 与 Electron 桌面端中被广泛用于把 Vue 响应式 ref 与浏览器localStorage双向绑定实现设置项、UI 偏好、权限记录等数据的自动持久化。读完本文你将掌握useLocalStorage的完整类型签名、与useStorage的委托关系、默认值合并、自定义序列化、全部可配置选项以及 airi 仓库中基于它封装的useLocalStorageManualReset工具与测试验证思路。useLocalStorage 是什么响应式 localStorage 绑定useLocalStorage是 VueUse 中 State状态分类下的一个组合式函数它返回一个可移除的 refRemovableRefT让开发者可以用读写普通 ref 的方式读写localStorage无需手工调用getItem/setItem/removeItem也不需要手动维护数据监听与序列化逻辑。在 airi 仓库的 VueUse Skills 决策指南 中它被标注为AUTO级别凡是 Vue 3 / Nuxt 3 项目里出现需要持久化的响应式状态都应优先考虑使用它而不是手写一套 localStorage 封装。它的姊妹函数还包括 useSessionStorage绑定sessionStorage与 useStorageAsync支持异步存储源。其中useSessionStorage的文档与useLocalStorage一样明确写着Please refer touseStorage因此理解useStorage就等于理解了这三个函数的核心机制。useLocalStorage的完整类型声明来自 useLocalStorage.mdexport declare function useLocalStorage( key: MaybeRefOrGetterstring, initialValue: MaybeRefOrGetterstring, options?: UseStorageOptionsstring, ): RemovableRefstring export declare function useLocalStorage( key: MaybeRefOrGetterstring, initialValue: MaybeRefOrGetterboolean, options?: UseStorageOptionsboolean, ): RemovableRefboolean export declare function useLocalStorage( key: MaybeRefOrGetterstring, initialValue: MaybeRefOrGetternumber, options?: UseStorageOptionsnumber, ): RemovableRefnumber export declare function useLocalStorageT( key: MaybeRefOrGetterstring, initialValue: MaybeRefOrGetterT, options?: UseStorageOptionsT, ): RemovableRefT export declare function useLocalStorageT unknown( key: MaybeRefOrGetterstring, initialValue: MaybeRefOrGetternull, options?: UseStorageOptionsT, ): RemovableRefT三个参数的含义分别为参数类型说明keyMaybeRefOrGetterstringlocalStorage 中存储用的键名可以传字符串、ref 或 getter 函数initialValueMaybeRefOrGetterT初始值 / 默认值类型决定返回的 ref 泛型optionsUseStorageOptionsT可选配置项控制深度监听、跨标签页同步、序列化等行为注意MaybeRefOrGetterT意味着key与initialValue都既可以是普通值也可以是 ref 或 getter——这是 VueUse 组合式函数一贯的灵活入参风格后面会看到key的响应式用法如何派上用场。基本用法把状态绑定到 localStorage由于useLocalStorage委托给useStorage实现useStorage.md 中演示的用法可以直接套用且默认存储源就是localStorageimport { useLocalStorage } from vueuse/core // 绑定对象 const state useLocalStorage(my-store, { hello: hi, greeting: Hello }) // 绑定布尔值 const flag useLocalStorage(my-flag, true) // 返回 Refboolean // 绑定数字 const count useLocalStorage(my-count, 0) // 返回 Refnumber // 删除存储中的数据置为 null 即触发 removeItem state.value null几点关键行为双向同步修改返回的 ref会自动序列化并写入localStoragelocalStorage被其他标签页或代码修改时ref 也会自动更新。置空即删除把 ref 的值设为null会从存储中移除该键这也是RemovableRef可移除 ref名称的由来。类型推导传什么类型的initialValue就返回对应泛型的 reftrue/0/str/ 对象分别得到Refboolean/Refnumber/Refstring/ 对象类型的 ref。Nuxt 3 注意事项在 Nuxt 3 项目中使用时需要留意该函数不会被自动导入因为 Nuxt 优先使用 Nitro 内置的useStorage()。若想使用 VueUse 的版本必须显式import { useLocalStorage } from vueuse/core。airi 中的典型用法在 airi 仓库中useLocalStorage被大量用于设置项持久化。例如 server-channel.ts 中持久化 WebSocket 服务器连接配置import { useLocalStorage } from vueuse/core const tlsConfig useLocalStorage{ cert?: string, key?: string, passphrase?: string } | null | undefined(settings/server-channel/websocket-tls-config, null) const hostname useLocalStoragestring(settings/server-channel/hostname, 127.0.0.1) const authToken useLocalStoragestring(settings/server-channel/auth-token, )这里使用了统一的settings/...前缀键名体系来避免键冲突controls-island.ts 中则持久化 UI 偏好const fadeOnHoverEnabled useLocalStorageboolean(controls-island/fade-on-hover-enabled, false) const dontShowItAgainNoticeFadeOnHover useLocalStorageboolean(preferences/dont-show-it-again/notice/fade-on-hover, false)而 InteractiveArea.vue 用useLocalStorageSendMode(ui/chat/settings/send-mode, enter)记住聊天发送模式notifications.vue 用useLocalStorage(devtools/notifications/title, )记住调试面板输入permissions-panel.vue 用useLocalStorage(permissions/microphone/requested, false)记录麦克风权限是否已请求过以避免重复弹窗。这些例子共同印证了该函数在真实工程中用户偏好 会话配置 一次性提示三大典型落地场景。默认值合并mergeDefaults默认情况下useLocalStorage会优先采用存储中已有的值完全忽略默认值。这带来一个隐患当你在代码中给默认值对象新增了属性而用户浏览器里早已存有旧结构的数据时新属性读出来会是undefined。// 假设 localStorage 中已有 {hello: hello} localStorage.setItem(my-store, {hello: hello}) const state useLocalStorage(my-store, { hello: hi, greeting: hello }, localStorage) console.log(state.value.greeting) // undefined因为存储中没有该键解决方案是开启mergeDefaults选项// 假设 localStorage 中已有 {hello: nihao} localStorage.setItem(my-store, {hello: nihao}) const state useLocalStorage( my-store, { hello: hi, greeting: hello }, localStorage, { mergeDefaults: true }, // -- 关键 ) console.log(state.value.hello) // nihao来自存储 console.log(state.value.greeting) // hello来自合并后的默认值合并策略说明设为true时对对象执行浅合并shallow merge存储中存在的键以存储值为准存储中缺失的键从默认值补全。也可以传入一个自定义合并函数例如实现深合并const state useLocalStorage( my-store, { hello: hi, greeting: hello }, localStorage, { mergeDefaults: (storageValue, defaults) deepMerge(defaults, storageValue) }, // -- 自定义深合并 )在 airi 的 server-channel.test.ts 中可以看到相关测试对tlsConfig等字段的null默认值与持久化读回行为的完整断言说明存储值 vs 默认值的优先级博弈正是真实工程里需要反复验证的边界场景。自定义序列化掌控读写格式默认情况下useLocalStorage会根据initialValue的类型智能选择序列化器对象使用JSON.stringify/JSON.parse数字使用Number.toString/parseFloat等。你也可以完全接管序列化逻辑传入自定义serializerimport { useLocalStorage } from vueuse/core useLocalStorage( key, {}, undefined, { serializer: { read: (v: any) v ? JSON.parse(v) : null, write: (v: any) JSON.stringify(v), }, }, )一个重要的坑当默认值传null时useLocalStorage无法从null推断数据类型此时必须显式提供自定义序列化器或直接复用内置序列化器import { StorageSerializers, useLocalStorage } from vueuse/core const objectLike useLocalStorage(key, null, undefined, { serializer: StorageSerializers.object }) objectLike.value { foo: bar }内置序列化器 StorageSerializers通过StorageSerializers可以引用全部内置序列化器序列化器类型说明string字符串纯字符串直存number数字经parseFloat解析boolean布尔布尔值object对象JSON 对象 / 数组mapMapJavaScriptMapsetSetJavaScriptSetdateDateJavaScriptDate经toISOStringany任意原始字符串直通例如持久化一个Mapimport { StorageSerializers, useLocalStorage } from vueuse/core const myMap useLocalStorage(my-map, new Map(), undefined, { serializer: StorageSerializers.map, })这在 airi 中是有实际需求的Map是 JSON 无法原生序列化的类型若不做处理直接存储读回时会退化成普通对象。airi 仓库 use-local-storage-manual-reset/index.test.ts 的测试中就使用new Mapstring, string()作为持久化状态侧面印证了Map类数据结构在真实项目中的使用频率。选项详解UseStorageOptions 全参数useLocalStorage的第三参options类型为UseStorageOptionsT全部选项如下来自 useStorage.md 的 Type DeclarationsuseLocalStorage(key, defaults, { // 深度监听对象/数组内部变化 (默认: 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 的 flush 时机 (默认: pre) flush: pre, })各选项的语义与默认值选项类型默认值作用deepbooleantrue是否深度监听对象 / 数组内部变化关闭后仅监听引用替换listenToStorageChangesbooleantrue是否监听storage事件用于多标签页应用同步writeDefaultsbooleantrue存储中不存在该键时是否把默认值写回存储mergeDefaultsboolean \| ((storageValue, defaults) T)false是否将存储值与默认值合并见上文默认值合并serializerSerializerT按类型智能选择自定义读写序列化器onError(error: unknown) voidconsole.error读写出错时的回调shallowbooleanfalse是否使用shallowRef作为底层引用initOnMountedbooleanfalse是否等组件挂载后再读取存储SSR 场景下很有用flush: pre表示监听器在组件更新前刷新与 Vue 默认的watch行为一致listenToStorageChanges在多标签页 / 多窗口应用例如 airi 的 Web 端与 Electron 桌面端并存中尤为关键——开启后一个页面修改状态其他标签页会自动收到同步。响应式 Key动态切换存储位置key支持 ref 或 getter当 key 变化时会自动从新的存储位置读取数据import { useLocalStorage } from vueuse/core const userId ref(user-1) const userData useLocalStorage( () user-data-${userId.value}, { name: }, ) // 切换用户后自动从新的存储位置读取 userId.value user-2这在多用户 / 多实例场景下非常实用把用户 ID 拼进键名切换身份时无需重建状态。airi 的键名体系如settings/server-channel/...、controls-island/...、permissions/...、ui/chat/settings/...本质上也体现了同样的命名空间 语义化设计思路避免键冲突并提升可读性。仓库实践useLocalStorageManualReset 封装与测试airi 在packages/stage-shared中基于useLocalStorage封装了一个高阶组合式函数useLocalStorageManualReset见 index.ts把useLocalStorage与 VueUse 的refManualReset组合提供手动重置能力import { refManualReset, useLocalStorage } from vueuse/core import { toRaw, unref, watch } from vue export function useLocalStorageManualResetT( key: MaybeRefOrGetterstring, initialValue: MaybeRefOrGetterT, options?: UseStorageOptionsT WatchOptions, ): ManualResetRefReturnT { const value unref(initialValue) const localStorageState useLocalStorageT(key, value, options) const state refManualResetT(localStorageState) const { resume, pause } watch(state, newValue localStorageState.value newValue, options) if (options?.listenToStorageChanges ! false) { watch(localStorageState, (newValue) { // 只有存储侧发起的值才需要跨过边界写回实时状态 // 避免把同一次写入重复发布为新的状态变更 if (toRaw(newValue) toRaw(state.value)) return pause() state.value newValue resume() }, options) } return state }这个封装揭示了useLocalStorage的几个底层机制细节watch 双向桥接useLocalStorage返回的 ref 与业务 ref 之间通过watch互相同步这正是响应式绑定的实现方式。listenToStorageChanges的单向性当设置为false时状态仍然会写入存储但存储侧的变更不会写回实时状态——这是开发者可以主动控制的同步边界。避免重复变更写入localStorageState后storage ref 会以相同引用反射回 state配合refManualReset即使赋值相同引用也会触发因此需要用toRaw比较来过滤掉自己写回的噪声。对应的单元测试 index.test.ts 在 jsdom 环境中验证了禁用存储监听后持久化值不会反射回实时状态这一关键行为测试中state.value new Map([[card-1, ReLU]])后同步 watcher 只应触发一次expect(changes).toBe(1)且值正确保留。测试注释还记录了根因同步 store 用结构化克隆替换 Map 后持久化序列化再反射回 store会被 Pinia 误判为一次新的直接变更进而重复发布领域快照——这是useLocalStorage在大型状态管理体系中反射写入引发连锁反应的典型案例也解释了为何需要在封装层做单向隔离。从源码结构看这个封装被用于语言设置持久化use-language.ts 中useLocalStorageManualResetstring(settings/language, )说明需要手动重置的持久化设置是该工具的主要应用面。使用注意事项综合文档与 airi 仓库实践使用useLocalStorage时有几点值得注意SSR 环境localStorage仅在浏览器存在。服务端渲染时建议配合initOnMounted: true或确认函数在 SSR 下有安全降级airi 的各 Web 应用apps/stage-web、apps/stage-pocket等均在浏览器渲染层使用符合该前提。键名冲突多模块应用建议采用模块/子模块/语义名的前缀体系airi 中settings/...、preferences/...、controls-island/...即为此类约定避免键覆盖。敏感信息localStorage是明文存储不加密。airi 的 server-channel.ts 虽将 authToken 写入 localStorage但真实的安全边界应建立在服务端认证与会话机制之上不要在客户端存储中放置高敏感凭据。大对象 / 特殊类型注意序列化成本与Map/Set/Date等特殊类型需要显式选择序列化器null默认值必须搭配显式serializer。与状态管理的交互如useLocalStorageManualReset的测试所揭示当持久化 ref 反射写入同步 store 时可能产生重复变更需要通过listenToStorageChanges或封装层的单向隔离来控制同步边界。总结useLocalStorage是 VueUse 中实现响应式 localStorage 持久化的标准答案它把getItem/setItem/removeItem与序列化、跨标签页同步、深度监听全部封装进一个RemovableRef类型安全且开箱即用。在 airi 中它支撑了从 WebSocket 服务器配置、聊天发送模式到权限记录、UI 偏好等大量跨会话状态的持久化并进一步派生出了useLocalStorageManualReset这一高阶封装。理解它的类型签名、mergeDefaults、自定义序列化与各项选项你就能在 Vue 3 / Nuxt 3 项目中以最小代码量获得健壮的本地持久化能力。深入研读 useStorage.md、useStorageAsync.md 与 SKILL.md 中的函数决策表可以进一步构建完整的 VueUse 状态管理知识体系。【免费下载链接】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-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考