同页面 Storage 变化监听

6,424 阅读4分钟

最近做业务的时候遇到一个需求,页面分为多个步骤,每个步骤展示的内容都不相同。

其实拿到这种需求,适用主流的 react / vue 框架来做都是非常容易且不回遇到任何坑的。

但是由于团队技术选择、项目搭建平台、功能复用等方面的原因,并不能简单的通过框架来实现。

这种步骤和内容,但是上方我步骤条和下方的内容在分模块开发的时候都是完全独立的没有关联,当按模块开发完成后拼装成页面的时候才会放到同一个页面中,那么像这样的一个页面该如何实现步骤的改变以及下方内容的切换呢?

我首先想到的是通过 storage 来实现。

sessionStorage

设计的是通过改变 sessionStorage 的值,监听 sessionStorage 变化事件来实现。

确实有 sessionStorage 变化监听事件,但是理想是丰满的,显示却是...

浏览器的 storage 事件只能监听同源的两个页面之间的 storage 变化,什么意思呢?就是同源的 A 页面 和 B 页面,在 A 页面中监听变化,在 B 页面中更新 storage 值,会出发 A 页面中监听回调。

// A.html
window.addEventListener("storage", function (e) {
        alert(e.newValue);
});
// B.html
window.sessionStorage.setItem("currentStep", "4");

这显然不符合我的业务场景。

重写 setItem

经查阅资料,监听同一个页面中的 storage 变化,需要重写 setItem 方法。

var originalSetItem = sessionStorage.setItem;
sessionStorage.setItem = function(key, newValue){
      var setItemEvent = new Event("setItemEvent");
      setItemEvent.newValue = newValue;
      window.dispatchEvent(setItemEvent);
      originalSetItem.apply(this,arguments);
}
window.addEventListener("setItemEvent", function (e) {
    alert(e.newValue);
});
sessionStorage.setItem("name","wang");

跑一下试试吧。

切换流畅自如,业务流程逻辑都完美,开心!

静等测试完上线吧!

万恶IE

正当我无所事事的时候,测试同事搞事情了,IE 浏览器无法切换步骤!!!

我...

赶紧找来 windows 笔记本,打开 IE 浏览器一看究竟。

呀 !setItem 执行报错了!不是函数。

这就很尴尬了,怎么在 IE 上就变成字符串了呢?

重写 setItem 方法在Chrome上(准确的说在非IE上),是没问题的,通过重写 setItem 方法来监听 storage 的变化。

但是在IE上,它会将 setItem 当做 key,把重写的方法的字符串形式当做值写入localStorage中. 结果就变成这样了.

sessionStorage: {
    'setItem': 'function(key,newValue){var setItemEvent = new Event("setItemEvent")...}'
}

所以调用 sessionStorage.setItem() IE会报错。

尝试突破IE

针对IE 11 可以覆盖原型上的 setItem 方法,即

var originalSetItem = sessionStorage.setItem;
sessionStorage.__proto__.setItem = function(key,newValue){
  var setItemEvent = new Event("setItemEvent");
  setItemEvent.newValue = newValue;
  window.dispatchEvent(setItemEvent);
  originalSetItem.apply(this,arguments);
}

但是在 IE 10 及以下是没有 __proto__ 这个属性的,那么上面的方法就不行了。

针对于字符串,似乎可以通过 eval() 转换成可执行的方法;

let sessionStorageSetItem;
if (typeof window.sessionStorage.setItem === "string") {
  // sessionStorageSetItem = (new Function("", "return " + window.sessionStorage.setItem))();
  sessionStorageSetItem = eval("(" + window.sessionStorage.setItem + ")");
} else {
  sessionStorageSetItem = window.sessionStorage.setItem;
}
export { sessionStorageSetItem };

在更新 storage 的时候执行 sessionStorageSetItem

这下好了,所以浏览器都整挂了,react 都不支持了。

此路不通!

开发模式上场

思来想去, storage 变化的监听其实也就是一个发布订阅模式,既然修改浏览器 API 会带来种种问题,那为什么不换一种思路,不去改动API,而是自己去触发监听,那很容易就想到了发布订阅模式了。

export const StorageEmitter =  {
  listeners: {},
  on(event, fn) {
    (this.listeners[event] || (this.listeners[event] = [])).push(fn);
  },
  off(event, fn) {
    const eventList = this.listeners[event];
    eventList && eventList.length && (this.listeners[event] = eventList.filter((f) => f!==fn));
  },
  emit(event, ...arg) {
    this.listeners[event] && this.listeners[event].forEach(fn => fn.apply(this, arg));
  }
}

在各个模块中订阅

StorageEmitter.on("storageSetItem", handleStepChange);

在修改 storage 后触发布任务

window.sessionStorage.setItem("currentStep", "4");
StorageEmitter.emit("storageSetItem");

本地测试、部署测试环境、IE浏览器才测试,全部通过~

没想到在改写 setItem 中花费那么多精力没能解决的问题,却通过最常用到的开发模式轻松解决。

OK,到此这个问题就结束了。

总结

通过这次的问题发现新的规范、API 会给我的开发带来很大的开发效率提升,有些时候遇到一些场景也会被 API 限制,甚至想要改写或加强 API 的功能,进而出现更多的难以解决的问题,反而被限制了,不能轻易的转换思路另辟蹊径,大部分情况下最基础常用的方法经验和模式也行是最简单的解决方案。