news 2026/9/10 2:51:05

airi 中的响应式 SessionStorage 实践:深入解析 VueUse useSessionStorage 的组合式存储方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
airi 中的响应式 SessionStorage 实践:深入解析 VueUse useSessionStorage 的组合式存储方案

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-sama's 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 伴侣项目中,它承担着"会话级、关标签页即失效"的状态持久化职责,与useLocalStorageuseStorage共同构成一套完整的浏览器存储响应式方案。读完本文,你将掌握useSessionStorage的全部重载签名、与useStorage的关系、默认合并与自定义序列化等进阶能力,并能参考 airi 仓库内的真实封装(如useLocalStorageManualReset)和 OIDC 登录流程,在 Vue 3 项目中落地自己的响应式会话存储。

什么是 useSessionStorage:响应式地访问 SessionStorage

useSessionStorage创建一个响应式 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', '') // 绑定布尔值,返回 Ref<boolean> const guided = useSessionStorage('session/onboarding-done', false) // 绑定数字,返回 Ref<number> 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: MaybeRefOrGetter<string>, initialValue: MaybeRefOrGetter<string>, options?: UseStorageOptions<string>, ): RemovableRef<string> export declare function useSessionStorage( key: MaybeRefOrGetter<string>, initialValue: MaybeRefOrGetter<boolean>, options?: UseStorageOptions<boolean>, ): RemovableRef<boolean> export declare function useSessionStorage( key: MaybeRefOrGetter<string>, initialValue: MaybeRefOrGetter<number>, options?: UseStorageOptions<number>, ): RemovableRef<number> export declare function useSessionStorage<T>( key: MaybeRefOrGetter<string>, initialValue: MaybeRefOrGetter<T>, options?: UseStorageOptions<T>, ): RemovableRef<T> export declare function useSessionStorage<T = unknown>( key: MaybeRefOrGetter<string>, initialValue: MaybeRefOrGetter<null>, options?: UseStorageOptions<T>, ): RemovableRef<T>

要点说明:

  • key:存储键名,类型为MaybeRefOrGetter<string>,即可以传字符串、ref或 getter 函数(见下文"响应式 Key");
  • initialValue:初始默认值,同样支持 ref 或 getter;其类型决定返回的RemovableRef<T>泛型;
  • optionsUseStorageOptions<T>配置项(见"配置项详解");
  • RemovableRef<T>:比普通Ref<T>多一个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/baseUrlsettings/llm/apiKeysettings/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 useLocalStorageManualReset<T>( key: MaybeRefOrGetter<string>, initialValue: MaybeRefOrGetter<T>, options?: UseStorageOptions<T> & WatchOptions, ): ManualResetRefReturn<T> { const value = unref(initialValue) const localStorageState = useLocalStorage<T>(key, value, options) const state = refManualReset<T>(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家族直接相关的关键点:

  1. UseStorageOptions<T>被透传useLocalStorageManualResetoptions参数直接透传给底层的useLocalStorage,说明所有存储选项(deeplistenToStorageChangeswriteDefaults等)在封装层依然生效;
  2. listenToStorageChanges选项影响封装逻辑:当该选项不为false时,封装层额外建立了一条从 storage 到手动 ref 的反向同步通道——这正是useStorage内部"跨标签页 storage 事件监听"机制的延伸运用;
  3. 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 = useSessionStorage<OIDCFlowState | null>('auth/v1/oidc-flow-state', null) const flowParams = useSessionStorage<OIDCFlowParams | null>('auth/v1/oidc-flow-params', null) // 跳转前写入 flowState.value = { ... } flowParams.value = { ... } // 回调消费并删除 const state = flowState.value flowState.value = null // 等价于 sessionStorage.removeItem

配置项详解:UseStorageOptions

useSessionStorageoptions类型为UseStorageOptions<T>,完整定义可见 .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,对大对象可避免深响应式开销,适合只整体替换(不修改内部字段)的场景。
  • initOnMounted:SSR 场景下,挂载前读取可能拿到服务端环境,设为true可推迟到onMounted再初始化。
  • onError:存储读写抛错(如隐私模式、配额超限)时的回调,默认console.error
  • flush:watch 回调的触发时机,'pre'表示在组件更新前同步写回。

默认值合并(Merge Defaults):避免"存量数据缺字段"

useSessionStorage继承自useStorage的默认行为是:只要存储中存在该键,就直接使用存储值,忽略默认值。这意味着当你在新版本中为默认值对象增加了字段,老用户存储中的旧数据不会自动补齐这些字段,读取时会出现undefined

sessionStorage.setItem('my-store', '{"hello": "hello"}') const state = useSessionStorage('my-store', { hello: 'hi', greeting: 'hello' }) console.log(state.value.greeting) // undefined —— 存储中不存在该字段

解决办法是开启mergeDefaults

const state = useSessionStorage( 'my-store', { hello: 'hi', greeting: 'hello' }, { mergeDefaults: true }, // <-- 浅合并:存储值优先,缺失字段用默认值补齐 ) console.log(state.value.hello) // 'hello'(来自存储) console.log(state.value.greeting) // 'hello'(来自合并的默认值)

mergeDefaultstrue时,对对象执行的是浅合并;如需深合并(嵌套对象逐层补齐),可传入自定义合并函数:

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数字(经parseFloat
boolean布尔值
objectJSON 对象 / 数组
mapJavaScriptMap
setJavaScriptSet
dateJavaScriptDate(经toISOString
any原始字符串直通(不做转换)

例如存储Map

import { StorageSerializers, useSessionStorage } from '@vueuse/core' const myMap = useSessionStorage('session/my-map', new Map(), { serializer: StorageSerializers.map, })

响应式 Key:键名随 ref 变化自动迁移

useSessionStoragekey参数支持MaybeRefOrGetter<string>,因此键名本身也可以是响应式的——当键变化时,组合式函数会自动读取新位置的数据:

const userId = ref('user-1') const userData = useSessionStorage( () => `session/user-data-${userId.value}`, { name: '' }, ) // 键变化后,userData 自动切换为读取新键对应的存储值 userId.value = 'user-2'

这一能力适合"同一份逻辑、多份会话数据"的场景,例如按当前用户、当前路由或当前会话 ID 区分存储位置,无需手动销毁重建 ref。

注意事项与最佳实践

综合关联文档与 airi 仓库实践,使用useSessionStorage时有几点值得留意:

  1. Nuxt 3 下的命名冲突:与useStorage一样,在 Nuxt 3 中useSessionStorage不会被自动导入(VueUse 刻意让位于 Nitro 内置的useStorage()),需要显式import { useSessionStorage } from '@vueuse/core'。此注意事项记录于 .agents/skills/vueuse-functions/references/useStorage.md 的提示块。
  2. 会话边界 = 标签页边界sessionStorage按标签页隔离,弹窗、window.open出来的新窗口可能不共享数据。airi 的 OIDC 流程就为此在 apps/ui-server-auth/src/pages/sign-in.vue 做了专门处理。
  3. 删除数据用remove()或赋nullRemovableRefremove()useSessionStorage相对原生 API 的便利增强,等价于sessionStorage.removeItem
  4. Electron 等 WebView 环境注意 flush 时机:airi 在 packages/stage-shared/src/composables/use-local-storage-manual-reset/index.ts 中通过"storage 回写与手动 ref 隔离"规避了重启时存储未落盘导致的竞态,这一思路对sessionStorage同样适用。
  5. 与手写 API 的取舍:airi 在 OIDC 流程中直接使用原生sessionStorage(packages/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.vue),sessionStorage则承载着 OIDC PKCE 流程等会话内临时状态(packages/stage-ui/src/libs/auth-oidc.ts)。理解了这套"生命周期决定存储选型、useStorage 统一底层"的设计哲学后,你在自己的 Vue 3 项目中就能准确地在useSessionStorageuseLocalStorageuseStorage之间做出选择。

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 2:47:24

CANN/ge GetKernelArgs API文档

GetKernelArgs 【免费下载链接】ge GE&#xff08;Graph Engine&#xff09;是面向昇腾的图编译器和执行器&#xff0c;提供了计算图优化、多流并行、内存复用和模型下沉等技术手段&#xff0c;加速模型执行效率&#xff0c;减少模型内存占用。 GE 提供对 PyTorch、TensorFlow …

作者头像 李华
网站建设 2026/9/10 2:43:17

社区家政小程序开发公司排名,上门服务系统搭建

社区家政小程序开发公司排名&#xff0c;上门服务系统搭建当下社区上门家政服务行业持续升温&#xff0c;保洁养护、家电维修、居家护理、便民修缮等服务需求日益常态化。众多中小家政企业、社区服务站点都开始摒弃传统线下登记、公域平台入驻的运营模式&#xff0c;转向自主搭…

作者头像 李华
网站建设 2026/9/10 2:40:00

NILMTK实战:非侵入式负荷监测工具包的环境配置与数据分解

简介&#xff1a;面向非侵入式负荷监测&#xff08;NILM&#xff09;研究者的完整可运行项目包&#xff0c;基于REDD低频数据集&#xff0c;内置CO和FHMM两种分解预测方法&#xff0c;适合在PyCharm中直接导入调试。资源共1046个文件&#xff0c;460.46MB&#xff0c;以Python源…

作者头像 李华