vue vuex-persist跨域情况下火狐浏览器不支持使用localStorage的兼容

47 阅读5分钟

环境: 维护了5年的老项目:vue2 vuex 状态持久化使用的是:vuex-persist

遇到的问题:

在北京的一个客户使用了我们的系统的 审批表单嵌入页面,嵌入到了他们的一个系统中:使用了 火狐浏览器 + iframe 嵌入

具体是什么导致的问题,并没有排查到,推测是客户系统安全的设置,或者浏览器识别iframe为了广告之后禁止了使用LocalStorage,

如果直接访问 LocalStorage 会直接爆错:

kehuwfti.png 但无法使用LocalStorage的话,就需要兼容

解决方案

本地模拟:

问题在于,但凡访问 LocalStorage 都会爆错,所以需要本地模拟一个不支持 LocalStorage 的环境,来方便本地bug修复后的测试

那么通过重写get方法,来模拟一个不支持 LocalStorage 的环境,如下:

Object.defineProperty(window, 'localStorage', {
    configurable: true,
    get: function() {
     throw new Error('localStorage is not available');
    }
})

img_2025_04_29_14_40_42.png

方案一:重写 localStorage

window.localStorage = {
    getItem: function() {},
    setItem: function() {},
    removeItem: function() {},
    clear: function() {}
}

这个方式显然是不行的,因为访问 LocalStorage 会爆错

方案二:try catch中访问LocalStorage,如果不可用,则使用sessionStorage存储

因为数据必须是要存储的,如果不存储,客户端登录之后token的数据就只能存储在内存中,那么当客户刷新的时候又需要重新走登录的逻辑。这显然不合理,在客户的浏览器上测试验证发现 sessionStorage 是可用的,所以可以考虑使用 sessionStorage 来存储 用户数据

关键词1:LocalStorage 不可用。那么需要判断是否可用:

const isLocalStorageAvailable = () => {
  const testKey = 'testLocalStorage'
  try {
    localStorage.setItem(testKey, 'test')
    localStorage.removeItem(testKey)
    return true
  } catch (e) {
    return false
  }
}

关键词2:则使用sessionStorage。那么需要判断不可用之后往 sessionStorage 中存储数据

if (this.isLocalStorageAvailable()) {
  try {
    localStorage.setItem(key, value)
    return
  } catch (e) {
    console.warn('存储到 localStorage 失败,降级到 sessionStorage', e)
    this.localStorageAvailable = false
  }
}

那么如果 sessionStorage 也不可用,那么考虑使用内存来存储数据了

if (this.sessionStorageAvailable) {
  try {
    sessionStorage.setItem(key, value)
    return
  } catch (e) {
    console.warn('存储到 sessionStorage 失败,降级到内存存储', e)
    this.sessionStorageAvailable = false
  }
}
this.memoryStorage[key] = value

考虑是否能这么做???

  1. 主要出登录状态能保持住吗??
  • 可以的,需要额外处理状态持久化的 vuex-persist 的逻辑,让LocalStorage不可用时,持久化数据存储到 sessionStorage 中
  1. 能够全局的替换掉当前在使用的 LocalStorage 吗?查询系统当前在使用的场景,有70多处
  • 也可以,写一个工具方法,全局替换掉 LocalStorage 即可,最好是挂载在 window下的方便替换

结果如下:

;(function () {
  class SafeLocalStorage {
    constructor() {
      this.memoryStorage = {}
      this.localStorageAvailable = this._isStorageAvailable('localStorage')
      this.sessionStorageAvailable = this._isStorageAvailable('sessionStorage')
      if (!this.localStorageAvailable && !this.sessionStorageAvailable) {
        console.warn('localStorage 和 sessionStorage 均不可用,已自动降级为内存存储')
      } else if (!this.localStorageAvailable) {
        console.warn('localStorage 不可用,已自动降级为 sessionStorage')
      }
    }

    _isStorageAvailable(type) {
      try {
        const storage = window[type]
        const testKey = '__storage_test__'
        storage.setItem(testKey, testKey)
        storage.removeItem(testKey)
        return true
      } catch (e) {
        return false
      }
    }

    setItem(key, value) {
      if (this.localStorageAvailable) {
        try {
          localStorage.setItem(key, value)
          return
        } catch (e) {
          console.warn('存储到 localStorage 失败,降级到 sessionStorage', e)
          this.localStorageAvailable = false
        }
      }
      if (this.sessionStorageAvailable) {
        try {
          sessionStorage.setItem(key, value)
          return
        } catch (e) {
          console.warn('存储到 sessionStorage 失败,降级到内存存储', e)
          this.sessionStorageAvailable = false
        }
      }
      this.memoryStorage[key] = value
    }

    getItem(key) {
      if (this.localStorageAvailable) {
        try {
          const value = localStorage.getItem(key)
          if (value !== null) return value
        } catch (e) {
          console.warn('从 localStorage 读取失败,降级到 sessionStorage', e)
          this.localStorageAvailable = false
        }
      }
      if (this.sessionStorageAvailable) {
        try {
          const value = sessionStorage.getItem(key)
          if (value !== null) return value
        } catch (e) {
          console.warn('从 sessionStorage 读取失败,降级到内存存储', e)
          this.sessionStorageAvailable = false
        }
      }
      return this.memoryStorage[key] !== undefined ? this.memoryStorage[key] : null
    }

    removeItem(key) {
      if (this.localStorageAvailable) {
        try {
          localStorage.removeItem(key)
        } catch (e) {
          console.warn('从 localStorage 删除失败,降级到 sessionStorage', e)
          this.localStorageAvailable = false
        }
      }
      if (this.sessionStorageAvailable) {
        try {
          sessionStorage.removeItem(key)
        } catch (e) {
          console.warn('从 sessionStorage 删除失败', e)
          this.sessionStorageAvailable = false
        }
      }
      delete this.memoryStorage[key]
    }

    clear() {
      if (this.localStorageAvailable) {
        try {
          localStorage.clear()
        } catch (e) {
          console.warn('清空 localStorage 失败,降级到 sessionStorage', e)
          this.localStorageAvailable = false
        }
      }
      if (this.sessionStorageAvailable) {
        try {
          sessionStorage.clear()
        } catch (e) {
          console.warn('清空 sessionStorage 失败', e)
          this.sessionStorageAvailable = false
        }
      }
      this.memoryStorage = {}
    }

    keys() {
      if (this.localStorageAvailable) {
        try {
          return Object.keys(localStorage)
        } catch (e) {
          console.warn('获取 localStorage 键名失败,降级到 sessionStorage', e)
          this.localStorageAvailable = false
        }
      }
      if (this.sessionStorageAvailable) {
        try {
          return Object.keys(sessionStorage)
        } catch (e) {
          console.warn('获取 sessionStorage 键名失败', e)
          this.sessionStorageAvailable = false
        }
      }
      return Object.keys(this.memoryStorage)
    }

    hasKey(key) {
      return this.getItem(key) !== null
    }
  }
  const safeLocalStorage = new SafeLocalStorage()
  if (typeof window !== 'undefined') {
    window.safeLocalStorage = safeLocalStorage
  }
})()

使用:

window.safeLocalStorage.setItem('test', 'test')

注意点:需要保证 vuex-persist 的逻辑中存储位置在 localStorage 不可用的时候,需要只使用 sessionStorage 来存储,这样数据才能持久化。

另外上面的初始脚本要放在较高的优先级,在逻辑未有读取 LocalStorage 之前执行,否则在初始化的时候会报错。项目中是放置在 html 的 head 中的初始化脚本中

img_2025_04_30_14_20_32.png

效果

解决了用户在火狐浏览器中使用 iframe 嵌入系统后,LocalStorage 不可用的问题,保证了数据的一致性,并且刷新之后登录状态依然存在

但是遇到了依赖项目里面使用到了 LocalStorage 的问题, 但是对于依赖,我们不做处理,因为每次打包都得处理。只有 xx-vxe-table 依赖中的一个依赖使用到了 LocalStorage,导致报错,并且它本身也是做了 LocalStorage 的兼容

img_2025_04_30_14_41_38.png

修复方式很简单,本身是有检测函数在的,只不过没在try catch中去访问 window.localStorage


// function l(e){try{return e.setItem("__xe_t",1),e.removeItem("__xe_t"),!0}catch(e){return!1}}

function l(e, key){try{return e[key]setItem("__xe_t",1),e[key]removeItem("__xe_t"),!0}catch(e){return!1}}

// l(o, 'localStorage')
// l(o, 'sessionStorage')

这带来了额外的维护成本,因为每次打包都得处理。但是我们的原则是不修改第三方的依赖代码。最终决定让客户对这个依赖的资源自行修改,项目只负责兼容业务代码。

总结

客户使用的是 单点登录的方式来访问系统,所以要考虑callback页面的登录数据存储兼容

在修复过程中需要场景覆盖全面,否则很容易出现一直在登录或者白屏的问题

原始问题依然未有头绪,如果有朋友知道原因,欢迎留言