环境: 维护了5年的老项目:vue2 vuex 状态持久化使用的是:vuex-persist
遇到的问题:
在北京的一个客户使用了我们的系统的 审批表单嵌入页面,嵌入到了他们的一个系统中:使用了 火狐浏览器 + iframe 嵌入
具体是什么导致的问题,并没有排查到,推测是客户系统安全的设置,或者浏览器识别iframe为了广告之后禁止了使用LocalStorage,
如果直接访问 LocalStorage 会直接爆错:
但无法使用LocalStorage的话,就需要兼容
解决方案
本地模拟:
问题在于,但凡访问 LocalStorage 都会爆错,所以需要本地模拟一个不支持 LocalStorage 的环境,来方便本地bug修复后的测试
那么通过重写get方法,来模拟一个不支持 LocalStorage 的环境,如下:
Object.defineProperty(window, 'localStorage', {
configurable: true,
get: function() {
throw new Error('localStorage is not available');
}
})
方案一:重写 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
考虑是否能这么做???
- 主要出登录状态能保持住吗??
- 可以的,需要额外处理状态持久化的 vuex-persist 的逻辑,让LocalStorage不可用时,持久化数据存储到 sessionStorage 中
- 能够全局的替换掉当前在使用的 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 中的初始化脚本中
效果
解决了用户在火狐浏览器中使用 iframe 嵌入系统后,LocalStorage 不可用的问题,保证了数据的一致性,并且刷新之后登录状态依然存在
但是遇到了依赖项目里面使用到了 LocalStorage 的问题, 但是对于依赖,我们不做处理,因为每次打包都得处理。只有 xx-vxe-table 依赖中的一个依赖使用到了 LocalStorage,导致报错,并且它本身也是做了 LocalStorage 的兼容
修复方式很简单,本身是有检测函数在的,只不过没在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页面的登录数据存储兼容
在修复过程中需要场景覆盖全面,否则很容易出现一直在登录或者白屏的问题
原始问题依然未有头绪,如果有朋友知道原因,欢迎留言