SharedWorker 共享工作者线程

最近搞了个websocket通讯,但是发现在多开浏览器页签的时候ws也会多开一个,但是基于浏览器机制情况下 chrome会干掉新开的页签导致新页签查看GG了, 只能开一个页签这就很蛋疼了。(后来发现是因为自己的弹出告警的Notification调用了window.close(),但是这个知识点还是有用的,因为这个等于说共享了websocket信道,只使用一个websocket就可以通讯多页签,节省了连接资源,算是一个针对连接的优化啦)
然后百度了很久发现都是一些介绍通过websocket解决多页签通讯的文章(还要服务端配合),无奈之下啃了一下高程。
发现了这个-----SharedWorker


前言

SharedWorker 在当前客户端下创建一个共享线程,每一个页签的通讯通过其中的port来传递和接收。而且在其他页签重新创建新的SharedWorker的时候浏览器自己会检测自己是不是已经创建过这个线程了,如果有就直接复用了,这个是直接使用浏览器机制的(爽)。

设计的想法:第一次创建的SharedWorker的时候创建websocket通信 然后通过SharedWorker来分发接收到的信息。

还有就是浏览器兼容性 还不错 但是ie11不支持 但是还好我这边暂时不考虑兼容ie的情况,兼容ie的同学请划走


一、先上demo


SharedWorker demo gif


demo条件

  • 需要服务器环境运行。我这边使用的是vs code 插件Live Server(这玩意咋用自己百度下)可以看一下视频里面的地址是127开头的。
  • chrome浏览器(这个不用多说了)要提一点的是SharedWorker 文件里面的console和debugger是不会出现page1 和page2的控制台的,这个需要去专门看线程的地方查看。chrome浏览器通过chrome://inspect/#workers进入。看图:

上代码

  • SharedWorker.js
// 记个数
let count = 0;
// 把每个连接的端口存下来
const ports = [];

// 连接函数 每次创建都会调用这个函数
onconnect = (e) => {
  console.log("这里是共享线程展示位置");
  // 获取端口
  const port = e.ports[0];
  // 把丫存起来
  ports.push(port);
  // 监听方法
  port.onmessage = (msg) => {
    // 这边的console.log是看不到的 debugger也是看不到的 需要在线程里面看
    console.log("共享线程接收到信息:", msg.data, count);
    if (msg.data === "+") {
      count++;
    }
    // 循环向所有端口广播
    ports.forEach((p) => {
      p.postMessage(count);
    });
  };
};
  • page1.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>SharedWorker-page1</title>
  </head>
  <body>
    <h1>SharedWorker-page1</h1>
    <button id="btn">count++</button>
    <script>
      const btn = document.querySelector("#btn");
      // 兼容性判断
      if (!SharedWorker) {
        throw new Error("当前浏览器不支持SharedWorker");
      }
      // 创建
      const worker = new SharedWorker("./SharedWorker.js");
      // 启动
      worker.port.start();
      // 线程监听消息
      worker.port.onmessage = (e) => {
        console.log("page1共享线程计数值:", e.data);
      };
      btn.addEventListener("click", (_) => {
        worker.port.postMessage("+");
      });
    </script>
  </body>
</html>
  • page2.hrml
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>SharedWorker-page2</title>
  </head>
  <body>
    <h1>SharedWorker-page2</h1>
    <button id="btn">count++</button>
    <script>
      const btn = document.querySelector("#btn");
      // 兼容性判断
      if (!SharedWorker) {
        throw new Error("当前浏览器不支持SharedWorker");
      }
      // 创建
      const worker = new SharedWorker("./SharedWorker.js");
      // 启动
      worker.port.start();
      // 线程监听消息
      worker.port.onmessage = (e) => {
        console.log("page2共享线程计数值:", e.data);
      };
      btn.addEventListener("click", (_) => {
        worker.port.postMessage("+");
      });
    </script>
  </body>
</html>

上面的代码基本上就已经算是OK了。

二、详细讲SharedWorker

1.创建共享工作者线程

创建共享工作者线程非常常用的方式是通过加载 JavaScript 文件创建。此时,需要给 SharedWorker 构造函数传入文件路径,该构造函数在后台异步加载脚本并实例化共享工作者线程。

基于绝对路径创建共享工作者线程:
// JavaScript 线程文件
SharedWorker.js
const sharedWorker = new SharedWorker('https://xxx.com/emptySharedWorker.js'); 
console.log(sharedWorker); // SharedWorker {} 
可以修改为使用相对路径:但是这基于该文件在你项目中的位置
const sharedWorker = new SharedWorker('./emptyWorker.js'); 
console.log(sharedWorker); // SharedWorker {}

2.SharedWorker 标识与独占

共享工作者线程标识源自解析后的脚本 URL、工作者线程名称和文档源。(可以通过第二参数给SharedWorker 命名)

实例化一个共享工作者线程 
如果你的服务地址正好就是xxx.com那么这三种解析方式就是同一个线程,只会创建一个,类似同源策略
另外两个会在其原有线程上增加一个端口port(需要我们通过创建一个ports数组存起来,方便之后数据分发)
- 全部基于同源调用构造函数
- 所有脚本解析为相同的 URL 
- 所有线程都有相同的名称
new SharedWorker('./sharedWorker.js'); 
new SharedWorker('sharedWorker.js'); 
new SharedWorker('https://xxx.com/sharedWorker.js');

如果当其中URL、工作者线程名称和文档源变更时候都会创建新的线程。

  • 改变url这个好理解(我就不演示了)
  • 改变文档源
demo中我又创建了一个page3.html
和另一个SharedWorker2.js
// 创建
page3与page1中唯一不同的就是引用了SharedWorker2.js
const worker = new SharedWorker("./SharedWorker2.js");
  • 呈现的结果是出现两个线程
  • 改变名字
demo中我又创建了一个page4.html
// 创建
page4和page2中唯一不同的就是给了不同的第2个名字(两种写法,效果相同,只不过对象还能传递其他参数)
page2中(直接给字符串)
 const worker = new SharedWorker("./SharedWorker.js",'page2');
page4中(给了对象)
 const worker = new SharedWorker("./SharedWorker.js",{name:'page4'});
  • 呈现的结果是出现两个线程

所以要注意的点就是如果要共享使用一个线程的话就要保证URL 源文件 以及名称相同。

3.使用 SharedWorker 对象

  • SharedWorker()构造函数返回的 SharedWorker 对象被用作与新创建的共享工作者线程通信的
    连接点。

它可以用来通过 MessagePort 在共享工作者线程和父上下文间传递信息,也可以用来捕获共
享线程中发出的错误事件。

  • SharedWorker 对象支持以下属性:

onerror:在共享线程中发生 ErrorEvent 类型的错误事件时会调用指定给该属性的处理程序。
  此事件会在共享线程抛出错误时发生。
  此事件也可以通过使用 sharedWorker.addEventListener(‘error’, handler)处理。
port:专门用来跟共享线程通信的 MessagePort。

4. SharedWorkerGlobalScope

在共享线程内部,全局作用域是 SharedWorkerGlobalScope 的实例。SharedWorkerGlobal-Scope 继承自 WorkerGlobalScope,因此包括它所有的属性和方法。可以通过 self 关键字访问该全局上下文。(例如刚刚demo中使用的name:page2/page4 可以在SharedWorker文件夹中通过self.name拿到)(我就是用这个传参的)(close全局关闭方法通过self.close()调用

在SharedWorker中使用websocket

看到这里基本上已经理解SharedWorker 的机制了,websocket这个直接拿代码说话。

// 为了尽量的让浏览器识别一下我这边尽我所能的写了es5
// 存储端口
var portList = []
// 存下来wsUrl用来重新连接使用
var websocketUrl = null
// ws实例
var websocket = null
// 当前错误次数
var webSocketError = 0
// 重连最大次数
var webSocketErrorMax = 6
// 我这边长连接保持使用的是计时器没有使用延时器 主要原因是后端不想跟我通心跳。。。。
var timeout = null
// 计时器发送时间
var delay = 30000
// 连接端  每次创建也就是new SharedWorker 都会调用onconnect
onconnect = function (e) {
  var port = e.ports[0]
  // 存储端口
  portList.push(port)
  // 监听port推送
  port.addEventListener('message', function (e) {
    // 取数据
    var data = e.data || {}
    console.log(data, portList, portList.indexOf(port))
    switch (data) {
    case 'open':
      // 创建websocket
      initSocket(self.name)
      break
    case 'portClose':
      // 关闭当前端口(new SharedWorker 会默认开启端口)
      if (portList.indexOf(port) > -1) {
        portList.splice(portList.indexOf(port), 1)
      }
      break
    case 'WsClose':
      // 关闭websocket
      websocket.send('close')
      websocketClose()
      websocket = null
      break
    case 'close':
      // 关闭SharedWorker 通过self调用 SharedWorkerGlobalScope 的实例
      self.close()
      break
    default:
      break
    }
    // 设置不抛出状态值
    if (['open', 'portClose', 'WsClose', 'close'].indexOf(data) === -1) {
      // 循环端口触发通讯
      for (var i = 0; i < portList.length; i++) {
        var eElement = portList[i]
        eElement.postMessage(e.data)
      }
    }
  })
  port.start()
}
// websocket创建
var initSocket = function (url) {
  if (!window.WebSocket) {
    window.WebSocket = window.MozWebSocket
  }
  if (websocket === null) {
    websocket = new WebSocket(url) // WebSocket服务端地址
    websocketUrl = url
  }
  websocket.onopen = function(event) {
    for (var i = 0; i < portList.length; i++) {
      var eElement = portList[i]
      eElement.postMessage(event.data)
    }
    webSocketError = 0
    heartCheck()
  }
  websocket.onmessage = function(event) {
    for (var i = 0; i < portList.length; i++) {
      var eElement = portList[i]
      eElement.postMessage(event.data)
    }
    // item!=port&&item.postMessage(workerResult);  /**不发给自己 */
  }
  websocket.onclose = function() {
    if (webSocketError < webSocketErrorMax) {
      setTimeout(function() {
        initSocket(websocketUrl)
      }, 5000)
    } else {
      console.warn('已达到最大重连次数')
    }
  }
  websocket.onerror = function() {
    webSocketError++
  }
}
// 心跳
function heartCheck() {
  timeout = setInterval(function() {
    websocket.send('1')
  }, delay)
}
// websocket关闭
function websocketClose() {
  websocket.send('close')
  webSocketError = 33
  clearInterval(timeout)
  websocket.close()
}
  • 这边就是用过SharedWorker name传参开启的SharedWorker 线程
  • 以及启动的websocket服务

这样之后两个页签就通过一个websocket和共享工作者线程实现同步推送了。

在vue中的使用

在vue中的使用我这边是放在了vuex里面。一个简单的store module附上
需要注意的点是:
SharedWorker的js文件是需要让各个浏览器页签引用的。
我这边是vue-cli3所以将文件放在了public中,这样不会被webpack打包给其他代码合并了。
再有就是引入public文件时候默认使用的路径是“ / ”(一个斜杠),这跟你项目vue.config文件的publicPath有关,自己请根据自己的配置来变更

const state = {
  sharedWorker: null,
  msg: null,
}

const mutations = {
  setSharedWorker: (state, data) => {
    state.sharedWorker = data
  },
  setMsg: (state, data) => {
    state.msg = data
  },
}

const actions = {
  SharedWorkerInit({ commit }, url) {
    if (!window.SharedWorker) {
      throw new Error('当前浏览器不支持SharedWorker')
    }
    // 启动SharedWorker
    const worker = new SharedWorker('/SharedWorker.js', url)
    // 定义接收方法
    worker.port.onmessage = (e) => {
      const { data } = e
      commit('setMsg', JSON.parse(data))
    }
    // 说的是默认打开端口 我发现有时候会莫名的不打开就加上了这一句
    worker.port.postMessage('open')
    commit('setSharedWorker', worker )
  },
  SharedWorkerClose({ state, commit }) {
    if (state.sharedWorker) {
      // 关闭当前端口 其实可以不关闭 但是这样写显得专业些
      state.sharedWorker.port.postMessage('close')
      // 关闭整个SharedWorker
      state.sharedWorker.close()
      commit('setSharedWorker', null )
    }
  },
  WsClose({ state }) {
    // 为了单独控制关闭websocket
    if (state.sharedWorker) {
      state.sharedWorker.port.postMessage('WsClose')
    }
  },
  PortClose({ state }) {
    // 仅关闭当前端口
    if (state.sharedWorker) {
      state.sharedWorker.port.postMessage('portClose')
    }
  },
}

export default {
  state,
  mutations,
  actions,
}