airi 中的响应式 SessionStorage 实践深入解析 VueUse useSessionStorage 的组合式存储方案【免费下载链接】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导读useSessionStorage是 VueUse 中用于将浏览器sessionStorage与 Vue 响应式系统绑定的组合式函数composable。在 airi 这类面向 Web / macOS / Windows 多端交付的自托管 AI 伴侣项目中它承担着会话级、关标签页即失效的状态持久化职责与useLocalStorage、useStorage共同构成一套完整的浏览器存储响应式方案。读完本文你将掌握useSessionStorage的全部重载签名、与useStorage的关系、默认合并与自定义序列化等进阶能力并能参考 airi 仓库内的真实封装如useLocalStorageManualReset和 OIDC 登录流程在 Vue 3 项目中落地自己的响应式会话存储。什么是 useSessionStorage响应式地访问 SessionStorageuseSessionStorage创建一个响应式 ref用于读写浏览器的sessionStorage的函数表中标注为AUTO调用级别——即在适用场景下可直接自动使用与useLocalStorage同表AUTO并列而其底层通用实现则是useStorage。其核心行为特点与sessionStorage原生 API 一致数据随标签页Tab会话存活关闭标签页或浏览器窗口即被清除不跨会话持久不跨标签页共享每个标签页拥有独立的存储空间不会持久化到磁盘刷新页面后仍在但关闭页面后消失。因此useSessionStorage适合保存本次会话内有效的状态例如表单草稿、一次性引导onboarding标记、OAuth 流程中的临时参数、当前会话的筛选条件等。凡是需要跨会话保留的用户偏好如语言设置、主题、API 地址则应改用useLocalStorage。基本用法一行代码绑定会话状态useSessionStorage的用法与useStorage完全一致关联文档明确指出Please refer touseStorage只是默认绑定到sessionStorage无需再传入第三个 storage 参数import { useSessionStorage } from vueuse/core // 绑定字符串 const token useSessionStorage(session/token, ) // 绑定布尔值返回 Refboolean const guided useSessionStorage(session/onboarding-done, false) // 绑定数字返回 Refnumber const step useSessionStorage(session/wizard-step, 0) // 绑定对象自动使用 JSON 序列化 const filters useSessionStorage(session/filters, { keyword: , page: 1 })赋值即写回存储读取即自动反序列化且值的变化会在同一标签页内被响应式地观察filters.value { keyword: airi, page: 2 } // 自动 sessionStorage.setItem(...) // 置为 null 会从存储中删除该键 filters.value null完整类型声明继承自原文档关联文档给出了useSessionStorage的全部重载签名按初始值类型自动推导返回的 ref 类型export declare function useSessionStorage( key: MaybeRefOrGetterstring, initialValue: MaybeRefOrGetterstring, options?: UseStorageOptionsstring, ): RemovableRefstring export declare function useSessionStorage( key: MaybeRefOrGetterstring, initialValue: MaybeRefOrGetterboolean, options?: UseStorageOptionsboolean, ): RemovableRefboolean export declare function useSessionStorage( key: MaybeRefOrGetterstring, initialValue: MaybeRefOrGetternumber, options?: UseStorageOptionsnumber, ): RemovableRefnumber export declare function useSessionStorageT( key: MaybeRefOrGetterstring, initialValue: MaybeRefOrGetterT, options?: UseStorageOptionsT, ): RemovableRefT export declare function useSessionStorageT unknown( key: MaybeRefOrGetterstring, initialValue: MaybeRefOrGetternull, options?: UseStorageOptionsT, ): RemovableRefT要点说明key存储键名类型为MaybeRefOrGetterstring即可以传字符串、ref或 getter 函数见下文响应式 KeyinitialValue初始默认值同样支持 ref 或 getter其类型决定返回的RemovableRefT泛型optionsUseStorageOptionsT配置项见配置项详解RemovableRefT比普通RefT多一个remove()方法调用后会将对应存储键删除并将值重置为默认值。useSessionStorage 与 useStorage / useLocalStorage 的关系useSessionStorage并非独立实现而是useStorage的便捷封装。从 .agents/skills/vueuse-functions/references/useStorage.md 的声明可以看出useStorage的第三个参数storage?: StorageLike允许指定任意类 Storage 对象默认使用localStorage// 等价写法一useSessionStorage 封装的正是下面的调用 const s1 useSessionStorage(key, value) // 等价写法二显式传入 sessionStorage const s2 useStorage(key, value, sessionStorage) // 等价写法三绑定 localStorage const s3 useStorage(key, value, localStorage)三者的选用原则可以概括为函数默认绑定目标数据生命周期典型场景useSessionStoragesessionStorage标签页会话内有效关闭即清除OAuth 临时参数、表单草稿、向导步骤useLocalStoragelocalStorage跨会话持久无过期时间语言、主题、API 配置等长期偏好useStoragelocalStorage可指定取决于传入的 storage需要自定义存储源或统一抽象时airi 仓库中的应用分布也印证了这一分工长期偏好语言设置、LLM 服务配置、聊天发送模式、弹窗不再提示标记统一走useLocalStorage例如 apps/component-calling/src/pages/index.vue 中用useLocalStorage持久化settings/llm/baseUrl、settings/llm/apiKey、settings/llm/model而 OIDC 登录这类跨页面跳转、会话内有效的临时状态则直接使用原生sessionStorage详见下文实战案例。实战案例一airi 对 useStorage 家族的企业级封装useLocalStorageManualReset虽然 airi 中未直接散落调用useSessionStorage但它对同族函数useLocalStorage做了一层很有参考价值的封装位于 packages/stage-shared/src/composables/use-local-storage-manual-reset/index.ts。这套封装思路完全可以迁移到useSessionStorage上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) { // 只有源自 storage 的值才需要回写 state // 避免手动 ref 因赋同一引用而触发第二次 Pinia mutation if (toRaw(newValue) toRaw(state.value)) return pause() state.value newValue resume() }, options) } return state }这段源码揭示了三个与useStorage家族直接相关的关键点UseStorageOptionsT被透传useLocalStorageManualReset的options参数直接透传给底层的useLocalStorage说明所有存储选项deep、listenToStorageChanges、writeDefaults等在封装层依然生效listenToStorageChanges选项影响封装逻辑当该选项不为false时封装层额外建立了一条从 storage 到手动 ref 的反向同步通道——这正是useStorage内部跨标签页 storage 事件监听机制的延伸运用watch与 ref 的联动通过watch(state, newValue localStorageState.value newValue)实现改动即持久化与useStorage自身watch 变化并写回存储的行为一致。该封装的消费方是 apps/stage-tamagotchi/src/renderer/composables/use-language.ts用于持久化settings/language并在注释中明确说明动机是规避Electron 重启时 renderer 的 localStorage 可能尚未 flush的问题。对应测试 apps/stage-tamagotchi/src/renderer/composables/use-language.test.ts 也验证了这套同步与恢复逻辑。如果你的需求是关闭标签页即丢弃的临时状态把封装内的useLocalStorage换成useSessionStorage即可获得同样健壮的会话级持久化。实战案例二sessionStorage 在 airi OIDC 登录流程中的运用useSessionStorage的底层存储sessionStorage有一个关键特性——同标签页内页面导航后数据仍保留这与 OAuth/PKCE 流程跳转到授权服务器再跳回的场景天然契合。airi 的 packages/stage-ui/src/libs/auth-oidc.ts 正是这样用的// Session storage keys for PKCE flow state (survives page navigation during OAuth) const FLOW_STATE_KEY auth/v1/oidc-flow-state const FLOW_PARAMS_KEY auth/v1/oidc-flow-params export function persistFlowState(flowState: OIDCFlowState, params: OIDCFlowParams): void { sessionStorage.setItem(FLOW_STATE_KEY, JSON.stringify(flowState)) sessionStorage.setItem(FLOW_PARAMS_KEY, JSON.stringify(params)) } export function consumeFlowState(): { flowState: OIDCFlowState, params: OIDCFlowParams } | null { const flowStateRaw sessionStorage.getItem(FLOW_STATE_KEY) const paramsRaw sessionStorage.getItem(FLOW_PARAMS_KEY) if (!flowStateRaw || !paramsRaw) return null sessionStorage.removeItem(FLOW_STATE_KEY) sessionStorage.removeItem(FLOW_PARAMS_KEY) return { flowState: JSON.parse(flowStateRaw), params: JSON.parse(paramsRaw), } }该模块的注释点明了选型理由Session storage keys for PKCE flow state (survives page navigation during OAuth)。同时 apps/ui-server-auth/src/pages/sign-in.vue 和 apps/ui-server-auth/src/pages/verify-email.vue 也围绕邮件链接在新标签页打开导致 sessionStorage进而 PKCE flowState不可见这一边界场景做了处理说明。迁移到 useSessionStorage 的价值把上述手写的setItem/getItem/removeItem换成useSessionStorage后可获得自动 JSON 序列化、响应式联动、RemovableRef.remove()内置删除等能力而生命周期语义同标签页导航存活、新标签页隔离完全不变const flowState useSessionStorageOIDCFlowState | null(auth/v1/oidc-flow-state, null) const flowParams useSessionStorageOIDCFlowParams | null(auth/v1/oidc-flow-params, null) // 跳转前写入 flowState.value { ... } flowParams.value { ... } // 回调消费并删除 const state flowState.value flowState.value null // 等价于 sessionStorage.removeItem配置项详解UseStorageOptionsuseSessionStorage的options类型为UseStorageOptionsT完整定义可见 .agents/skills/vueuse-functions/references/useStorage.md各字段的作用与默认值如下useSessionStorage(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 触发时机默认 pre flush: pre, })逐个解读其含义deep控制对对象/数组的深度监听。默认true因此修改嵌套属性如filters.value.keyword x也会触发写回存储设false可减少大对象上的监听开销。listenToStorageChanges是否监听storage事件以同步多标签页变化。对useSessionStorage而言由于sessionStorage不跨标签页共享此选项的实际影响较小但接口层面保持一致。writeDefaults若存储中尚无该键是否把默认值写入存储。设为false可避免仅读取场景污染存储空间。shallow为true时内部使用shallowRef对大对象可避免深响应式开销适合只整体替换不修改内部字段的场景。initOnMountedSSR 场景下挂载前读取可能拿到服务端环境设为true可推迟到onMounted再初始化。onError存储读写抛错如隐私模式、配额超限时的回调默认console.error。flushwatch 回调的触发时机pre表示在组件更新前同步写回。默认值合并Merge Defaults避免存量数据缺字段useSessionStorage继承自useStorage的默认行为是只要存储中存在该键就直接使用存储值忽略默认值。这意味着当你在新版本中为默认值对象增加了字段老用户存储中的旧数据不会自动补齐这些字段读取时会出现undefinedsessionStorage.setItem(my-store, {hello: hello}) const state useSessionStorage(my-store, { hello: hi, greeting: hello }) console.log(state.value.greeting) // undefined —— 存储中不存在该字段解决办法是开启mergeDefaultsconst state useSessionStorage( my-store, { hello: hi, greeting: hello }, { mergeDefaults: true }, // -- 浅合并存储值优先缺失字段用默认值补齐 ) console.log(state.value.hello) // hello来自存储 console.log(state.value.greeting) // hello来自合并的默认值当mergeDefaults为true时对对象执行的是浅合并如需深合并嵌套对象逐层补齐可传入自定义合并函数const state useSessionStorage( my-store, { hello: hi, profile: { nickname: , avatar: } }, { mergeDefaults: (storageValue, defaults) deepMerge(defaults, storageValue), }, )自定义序列化与内置序列化器默认情况下useSessionStorage会根据初始值类型智能选择序列化方式对象用JSON.stringify/JSON.parse数字用Number.toString/parseFloat布尔值同理。你也可通过serializer选项完全接管读写逻辑useSessionStorage( key, {}, { serializer: { read: (v: any) v ? JSON.parse(v) : null, write: (v: any) JSON.stringify(v), }, }, )需要注意当默认值为null时useSessionStorage无法从 null 推断数据类型此时必须显式提供序列化器或复用内置序列化器。内置序列化器通过StorageSerializers暴露对应关系如下类型说明string普通字符串number数字经parseFloatboolean布尔值objectJSON 对象 / 数组mapJavaScriptMapsetJavaScriptSetdateJavaScriptDate经toISOStringany原始字符串直通不做转换例如存储Mapimport { StorageSerializers, useSessionStorage } from vueuse/core const myMap useSessionStorage(session/my-map, new Map(), { serializer: StorageSerializers.map, })响应式 Key键名随 ref 变化自动迁移useSessionStorage的key参数支持MaybeRefOrGetterstring因此键名本身也可以是响应式的——当键变化时组合式函数会自动读取新位置的数据const userId ref(user-1) const userData useSessionStorage( () session/user-data-${userId.value}, { name: }, ) // 键变化后userData 自动切换为读取新键对应的存储值 userId.value user-2这一能力适合同一份逻辑、多份会话数据的场景例如按当前用户、当前路由或当前会话 ID 区分存储位置无需手动销毁重建 ref。注意事项与最佳实践综合关联文档与 airi 仓库实践使用useSessionStorage时有几点值得留意Nuxt 3 下的命名冲突与useStorage一样在 Nuxt 3 中useSessionStorage不会被自动导入VueUse 刻意让位于 Nitro 内置的useStorage()需要显式import { useSessionStorage } from vueuse/core。此注意事项记录于 .agents/skills/vueuse-functions/references/useStorage.md 的提示块。会话边界 标签页边界sessionStorage按标签页隔离弹窗、window.open出来的新窗口可能不共享数据。airi 的 OIDC 流程就为此在 apps/ui-server-auth/src/pages/sign-in.vue 做了专门处理。删除数据用remove()或赋nullRemovableRef的remove()是useSessionStorage相对原生 API 的便利增强等价于sessionStorage.removeItem。Electron 等 WebView 环境注意 flush 时机airi 在 packages/stage-shared/src/composables/use-local-storage-manual-reset/index.ts 中通过storage 回写与手动 ref 隔离规避了重启时存储未落盘导致的竞态这一思路对sessionStorage同样适用。与手写 API 的取舍airi 在 OIDC 流程中直接使用原生sessionStoragepackages/stage-ui/src/libs/auth-oidc.ts因为该场景是一次性读写、无需响应式联动而凡是状态需要在组件模板或计算属性中联动、且需要自动序列化的场景useSessionStorage都是更简洁的选择——这正是 VueUse 组合式方案少写样板代码、聚焦业务的价值所在。总结useSessionStorage是 VueUse State 类别中面向会话级数据的标准答案它把sessionStorage的读写、序列化、删除与 Vue 的响应式系统无缝衔接重载签名覆盖string / boolean / number / 泛型 / null五种形态配置项与useStorage完全同源并支持默认值合并、自定义序列化与响应式键名。在 airi 中同族的useLocalStorage已被广泛用于语言、LLM 配置等长期偏好的持久化apps/component-calling/src/pages/index.vuesessionStorage则承载着 OIDC PKCE 流程等会话内临时状态packages/stage-ui/src/libs/auth-oidc.ts。理解了这套生命周期决定存储选型、useStorage 统一底层的设计哲学后你在自己的 Vue 3 项目中就能准确地在useSessionStorage、useLocalStorage、useStorage之间做出选择。【免费下载链接】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),仅供参考
