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,
}