SessionStorage在前端开发中的实际应用与常见陷阱总结

UX-梦媛 前端 阅读 602
赞 23 收藏
二维码
手机扫码查看
反馈

又踩坑了,SessionStorage 里存对象居然拿不出来

今天上线前测一个表单页,用户填了一半切到别的标签页,再切回来——草,数据全没了。我第一反应是:不是写了 sessionStorage.setItem('formState', JSON.stringify(data)) 吗?怎么取出来是 null

SessionStorage在前端开发中的实际应用与常见陷阱总结

这里我踩了个坑:一开始以为是 sessionStorage 被清掉了,查了浏览器开发者工具的 Application → Storage → Session Storage,发现 key 是在的,值也显示着一长串 JSON 字符串……但用 JSON.parse(sessionStorage.getItem('formState')) 就报错:Unexpected token u in JSON at position 0

折腾了半天发现,getItem 返回的是 null,但控制台里明明看着有值啊?后来试了下直接打印:console.log(sessionStorage.getItem('formState')),结果输出是 undefined —— 等等?刚才还在控制台里看到值的?

再点开那个 key 一看,原来是 Chrome DevTools 的 Session Storage 面板有个 bug:它会缓存旧状态,甚至在页面刷新后还显示上一次的值(尤其是你用了 location.reload() 或者 F5 刷新时)。真实值早就没了。我立刻在控制台执行:sessionStorage.clear(),再重新走一遍流程,果然——表单提交前没保存,切走再回来,getItem 就是 null

但问题不在“没存”,而在“存了但读不出来”。继续 debug:我在保存前加了日志:console.log('save:', JSON.stringify(data)),没问题;保存后立刻 console.log('get right after:', sessionStorage.getItem('formState')),输出正常。可等页面切走再切回来,在 mounted(Vue)或者 useEffect(React)里再读,就变成 null 了。

查 MDN、翻 Stack Overflow,才发现一个被很多人忽略的事实:sessionStorage 是以“源(origin)+ 浏览器 tab 进程”为边界的。重点来了:如果你用 window.open(url) 打开新窗口,或者用 target="_blank" 跳转,新页面和原页面属于同一个 session,共享 sessionStorage;但如果你是在当前 tab 里用 location.href = '/xxx'router.push('/xxx') 跳转,那没问题,还是同一个 session。

但我们这个项目用了 PWA + display: 'standalone',有个场景是:用户从桌面图标启动 App(本质是独立进程),然后点击某个按钮调用 window.open('https://jztheme.com/form') 打开表单页——注意!这个新开的窗口虽然 URL 是我们自己的域名,但它是一个全新的浏览器上下文,和 PWA 主窗口不共享 sessionStorage。所以表单页根本读不到主窗口存的数据。

还有更隐蔽的坑:iOS Safari 对 sessionStorage 的处理特别保守。哪怕你在同一个 tab 里用 history.pushState 切换路由,某些 iOS 版本(特别是 16.4–16.6)会在后台 tab 恢复时清掉 sessionStorage,尤其是当系统内存紧张的时候。我们 QA 在 iPhone 上反复复现了这个问题:切到微信聊两句再切回来,表单数据就丢了。

总结下来,问题根源其实不是“怎么存”,而是“谁在读、在哪读、什么时候读”。我们原来的设计太理想化了:假设 sessionStorage 永远可用、永远一致、永远跨 tab 同步——现实是它既不持久,也不跨进程,还不稳定。

最后怎么解决的?核心代码就这几行

我们没去搞复杂的 IndexedDB 或 localStorage fallback(因为明确要“关掉页面就丢”,不能用 localStorage),而是做了两件事:

  • 统一入口校验:所有需要恢复表单的页面,加载时先检查 sessionStorage.getItem('formState'),如果为 null,就主动触发一次“尝试从 URL 参数或 referrer 中还原”的逻辑;
  • 在关键跳转处,手动透传状态:比如从首页跳表单页,不用 window.open,改用 router.push({ path: '/form', state: { formData: data } })(Vue Router 4 的 state 支持),然后在表单页用 router.state.value 拿;如果是跨域或必须用 window.open,就拼 query 参数:window.open(/form?data=${encodeURIComponent(JSON.stringify(data))})
  • 最重要的一条:**所有写入 sessionStorage 的操作,都包一层 try/catch,并加日志上报**。这样下次再出问题,至少能知道是序列化失败、存储超限(5MB 限制)、还是被浏览器策略拦截了。

下面是实际落地的封装函数(Vue 3 Composition API):

// utils/storage.js
export const safeSetSessionStorage = (key, value) => {
  try {
    const str = typeof value === 'string' ? value : JSON.stringify(value)
    // iOS Safari 有些版本对单个 item 大小敏感,加个长度兜底
    if (str.length > 4 * 1024 * 1024) {
      console.warn([storage] ${key} too large: ${str.length} chars)
      return false
    }
    sessionStorage.setItem(key, str)
    return true
  } catch (e) {
    console.error([storage] set ${key} failed:, e)
    // 上报到监控系统(我们用 Sentry)
    // Sentry.captureException(e, { extra: { key, value } })
    return false
  }
}

export const safeGetSessionStorage = (key, fallback = null) => {
  try {
    const raw = sessionStorage.getItem(key)
    if (raw == null || raw === 'undefined' || raw.trim() === '') {
      return fallback
    }
    return JSON.parse(raw)
  } catch (e) {
    console.error([storage] parse ${key} failed:, e)
    return fallback
  }
}

export const clearSessionStorageByKey = (key) => {
  try {
    sessionStorage.removeItem(key)
  } catch (e) {
    console.error([storage] remove ${key} failed:, e)
  }
}

然后在表单组件里这么用:

// components/MyForm.vue
import { safeGetSessionStorage, safeSetSessionStorage } from '@/utils/storage'

const formData = ref(safeGetSessionStorage('formState', {
  name: '',
  email: '',
  message: ''
}))

const saveToSession = () => {
  safeSetSessionStorage('formState', {
    name: formData.value.name,
    email: formData.value.email,
    message: formData.value.message
  })
}

// 监听输入变化(防抖 500ms)
watch(formData, saveToSession, { deep: true })

另外补了一个小补丁:在页面 unload 前强制存一次(防止用户手快点了关闭按钮):

onBeforeUnmount(() => {
  window.removeEventListener('beforeunload', handleBeforeUnload)
})

const handleBeforeUnload = () => {
  safeSetSessionStorage('formState', formData.value)
}
window.addEventListener('beforeunload', handleBeforeUnload)

改完之后,iOS 和安卓都稳了。不过还有个小尾巴:如果用户开了多个同域名 tab,每个 tab 的 sessionStorage 是隔离的,所以他在 A tab 填一半切到 B tab 再切回来,A tab 还是会丢——但这属于合理预期,我们加了文案提示:“请勿同时打开多个表单页”。

还有一个细节:Chrome 最近(v124+)加了个新策略,如果页面被长时间冻结(比如标签页后台超过 5 分钟),可能自动清掉 sessionStorage。我们没遇到,但加了日志后,万一出现就能快速定位。

踩坑提醒:这三点一定注意

  • sessionStorage 不是“临时 localStorage”:它的生命周期和 tab 绑定,不是和“用户会话”绑定;
  • 别信 DevTools 的 Session Storage 面板:它经常显示过期状态,要用 sessionStorage.getItem 实际跑一遍;
  • iOS Safari 是重灾区:尤其 standalone 模式 + background restore 场景,建议必加 try/catch + fallback 逻辑。

以上是我踩坑后的总结,希望对你有帮助。如果你有更好的方案,比如用 BroadcastChannel 做多 tab 同步,或者更轻量的内存缓存策略,欢迎评论区交流。这个技巧的拓展用法还有很多,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论