实现浏览器跨Tab标签通信

740 阅读3分钟

背景

最近项目上有个需求要求不能同时开多个浏览器tab标签查看项目内网页,具体的操作行为是当已经打开一个tab标签查看后,如果用户再打开第二个tab进行登录查看,此时第一个tab标签页内的网页上弹出警告框(如下图),用户只能选择关闭第一个标签页或者选择刷新第一个网页跳转到登录页面,此需求的目的是防止用户对比两个tab标签页上的数据进而出现医疗事故。我们简单分析下技术实现,就是此文章的标题,如何实现浏览器跨Tab标签通信?

图片.png

实践探索

分析这个需求,不同tab标签间通信也分为两种情况,第一种,两个tab标签内的网页是同源的,第二种就是非同源网页,显然此需求是符合同源的,实现方式就有更多可能性。

  1. 第一种,websocket,此方案可以不用考虑两个页面是否跨域,但是此方案接入太重,暂时不考虑;
  2. 第二种,postMessage(),同样支持跨域通信,但是使用的前提条件是B页面要么是从A页面内通过window.open打开的,要么是作为A页面的iframe嵌的子页面,因此也不适合我这需求的场景;
  3. 第三种,cookie,cookie也是需要符合同源网页,其次它的存储数据偏小,时效是默认关闭页面就会失效,后台也可以设置过期时间,同时cookie的增删查改操作麻烦,并且cookie的js操作权限与httpOnly的设置相关,笔者项目内对cookie设置了httpOnly导致js内无法控制和操作cookie,当然可以通过谷歌调试工具进行外部操作,因此cookie也不能满足需求
  4. 持久化存储localstorage,localstorage同样需要符合同源策略,时效是用户主动清理缓存,能存储更多的数据,增删查改操作简单,符合当前需求的技术要求

具体实现方案

通过以上分析以及实践探索,最终选择的方案是通过localstorage的方案来存储核心变量并且配合定时器轮询来实现需求,核心代码如下:

import DialogAlert from "../component/Dialog/DialogAlert";
import GlobalTimeout from "../util/globalTimeout";
import Storage from "../util/storage";
export default function singleInstanceChecker() {
  let instance;
  

  function createInstance() {
    //设置token变量
    const loginToken = `epr_browser_${new Date().getTime()}`;
    //localstorage存储token
    Storage.setBrowserToken(loginToken);
    Storage.setBrowserRefreshToken(loginToken);

    const Checker = function() {
      let self = this;
      //闭包保存当前token变量,内存缓存
      this.localBrowserToken = loginToken;
      this.isDuplicated = false;
      this.validateToken = function() {
        const tokenMsg = Storage.getBrowserToken();
        if (tokenMsg) {
          //当前闭包内的token与localstorage存储token不一致说明在新的Tab内进行了登录并刷新了token
          if (self.localBrowserToken != tokenMsg && !self.isDuplicated) {
            self.isDuplicated = true;
            //监测已经打开新的Tab标签后应当关闭当前Tab的定时器
            GlobalTimeout.clearAllTimers();
            //插入警告框
            //禁止页面内所有dom元素的响应
            document.getElementById("root").style.pointerEvents = "none";
            const div = document.createElement("div");
            div.id = "detect_browser";
            document.body.appendChild(div);
            ReactDOM.render(
              <DialogAlert
                open={true}
                title="Warning"
                content=" A new xxx window is detected. Please close this expired xxx window."
              />,
              div
            );
          }
        }
      };

      this.init = function() {
      //每隔3s轮询判断token是否被新的Tab标签刷新
        GlobalTimeout.setInterval(self.validateToken, 3000);
      };
    };
    return new Checker();
  }

  return {
    //单例模式
    getInstance: function() {
      if (!instance) {
        instance = createInstance();
        instance.init();
      }
      return instance;
    },
    isDuplicated: function() {
      if (instance) {
        return instance.isDuplicated;
      } else {
        return false;
      }
    }
  };
}

总结

以上方案还涉及到当弹出警告框时也需要将当前页面内的dom元素的响应事件关闭以防止在调试面板上将警告框dom移除后进行其他操作,这里有一个css属性就可以实现,即pointerEvents:none。以上就是跨浏览器Tab标签通信的场景解决方案之一,供有需要的参考。