文章目录
- 前言
- 一、SharedWorker 是什么
- SharedWorker 是什么
- SharedWorker 的使用方式
- SharedWorker 标识与独占
- 二、Demo使用
- 三、使用SharedWorker实现WebSocket共享
前言
最近有一个需求,需要实现用户系统消息时时提醒功能。第一时间就是想用WebSocket进行长连接。但是前端项目点击跳转需要打开新的标签页。这个时间就会出现新的标签页打开会把老的WebSocket连接挤掉。然后就想到了去共享一个WebSocket连接。就能实现多个标签页消息共享了。
一、SharedWorker 是什么
SharedWorker 是什么
SharedWorker
是一种特殊类型的 Worker,可以被多个浏览上下文访问,比如多个 windows,iframes 和 workers,但这些浏览上下文必须同源。它们实现于一个不同于普通 worker 的接口,具有不同的全局作用域:SharedWorkerGlobalScope ,但是继承自WorkerGlobalScope
SharedWorker 的使用方式
SharedWorker 线程的创建和使用跟 worker 类似,事件和方法也基本一样。 不同点在于,主线程与 SharedWorker 线程是通过MessagePort建立起链接,数据通讯方法都挂载在SharedWorker.port上。
值得注意的是,如果你采用 addEventListener
来接收 message
事件,那么在主线程初始化SharedWorker()
后,还要调用 SharedWorker.port.start()
方法来手动开启端口。
// main.js(主线程)
const myWorker = new SharedWorker('./sharedWorker.js');
myWorker.port.start(); // 开启端口
myWorker.port.addEventListener('message', msg => {
console.log(msg.data);
})
但是,如果采用 onmessage
方法,则默认开启端口,不需要再手动调用SharedWorker.port.start()
方法
// main.js(主线程)
const myWorker = new SharedWorker('./sharedWorker.js');
myWorker.port.onmessage = msg => {
console.log(msg.data);
};
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'});
二、Demo使用
demo演示:
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实现WebSocket共享
SharedWorker.js
SharedWorker的js文件是需要让各个浏览器页签引用的。所以将文件放在了public中
// 记个数
let count = 0;
// 把每个连接的端口存下来
const ports = [];
var state = {
webSocket: null, // webSocket实例
lockReconnect: false, // 重连锁,避免多次重连
maxReconnect: 6, // 最大重连次数, -1 标识无限重连
reconnectTime: 0, // 重连尝试次数
heartbeat: {
interval: 30 * 1000, // 心跳间隔时间
timeout: 10 * 1000, // 响应超时时间
pingTimeoutObj: null, // 延时发送心跳的定时器
pongTimeoutObj: null, // 接收心跳响应的定时器
pingMessage: JSON.stringify({type: 'ping'}), // 心跳请求信息
},
token:null
}
// 连接函数 每次创建都会调用这个函数
onconnect = (e) => {
console.log("这里是共享线程展示位置", e);
// 获取端口
const port = e.ports[0];
// 把丫存起来
ports.push(port);
// 监听方法
port.onmessage = (msg) => {
// 这边的console.log是看不到的 debugger也是看不到的 需要在线程里面看
console.log("共享线程接收到信息:", msg);
var data = msg.data || {}
var conf = JSON.parse(data)
console.log("解析后的参数", conf)
switch (conf.type) {
case "open":
console.log("共享线程状态为Open")
if (!state.webSocket) {
state.token=conf.token
initWebSocket(conf.host, conf.baseURL, conf.uri, state.token, conf.tenant);
}
break
case 'portClose':
console.log("共享线程状态为portClose")
// 关闭当前端口(new SharedWorker 会默认开启端口)
if (ports.indexOf(port) > -1) {
ports.splice(ports.indexOf(port), 1)
}
break
case 'wsClose':
// 关闭websocket
console.log("共享线程状态为WsClose")
state.webSocket.close();
clearTimeoutObj(state.heartbeat);
state.websocket = null
state.token=null
break
case 'close':
// 关闭SharedWorker 通过self调用 SharedWorkerGlobalScope 的实例
console.log("共享线程状态为close")
self.close()
break
default:
break
}
};
};
const initWebSocket = (host, baseURL, uri, token, tenant) => {
// ws地址
let wsUri = `ws://${host}${baseURL}${uri}?access_token=${token}&TENANT-ID=${tenant}`;
// let wsUri = `ws://${host}${baseURL}${other.adaptationUrl(props.uri)}?access_token=${token.value}&TENANT-ID=${tenant.value}`;
// let wsUri = `ws://${host}${baseURL}${uri}?access_token=${token}`;
// 建立连接
state.webSocket = new WebSocket(wsUri);
// 连接成功
state.webSocket.onopen = onOpen;
// 连接错误
state.webSocket.onerror = onError;
// 接收信息
state.webSocket.onmessage = onMessage;
// 连接关闭
state.webSocket.onclose = onClose;
};
const reconnect = () => {
if (!state.token) {
return;
}
if (state.lockReconnect || (state.maxReconnect !== -1 && state.reconnectTime > state.maxReconnect)) {
return;
}
state.lockReconnect = true;
setTimeout(() => {
state.reconnectTime++;
// 建立新连接
initWebSocket();
state.lockReconnect = false;
}, 5000);
};
/**
* 清空定时器
*/
const clearTimeoutObj = (heartbeat) => {
heartbeat.pingTimeoutObj && clearTimeout(heartbeat.pingTimeoutObj);
heartbeat.pongTimeoutObj && clearTimeout(heartbeat.pongTimeoutObj);
};
/**
* 开启心跳
*/
const startHeartbeat = () => {
const webSocket = state.webSocket;
const heartbeat = state.heartbeat;
// 清空定时器
clearTimeoutObj(heartbeat);
// 延时发送下一次心跳
heartbeat.pingTimeoutObj = setTimeout(() => {
// 如果连接正常
if (webSocket.readyState === 1) {
//这里发送一个心跳,后端收到后,返回一个心跳消息,
webSocket.send(heartbeat.pingMessage);
// 心跳发送后,如果服务器超时未响应则断开,如果响应了会被重置心跳定时器
heartbeat.pongTimeoutObj = setTimeout(() => {
webSocket.close();
}, heartbeat.timeout);
} else {
// 否则重连
reconnect();
}
}, heartbeat.interval);
};
/**
* 连接成功事件
*/
const onOpen = () => {
console.log("连接成功")
//开启心跳
startHeartbeat();
state.reconnectTime = 0;
};
/**
* 连接失败事件
* @param e
*/
const onError = () => {
console.log("连接 失败")
//重连
reconnect();
};
/**
* 连接关闭事件
* @param e
*/
const onClose = () => {
//重连
reconnect();
};
/**
* 接收服务器推送的信息
* @param msgEvent
*/
const onMessage = (msgEvent) => {
//收到服务器信息,心跳重置并发送
console.log("接到消息", msgEvent)
startHeartbeat();
// const text = JSON.parse(msgEvent.data);
ports.forEach((p) => {
p.postMessage(msgEvent.data);
});
};
定义一个组件叫WebSocket.vue
代码中有一些token的判断可以无视。
我这里怎么简单怎么来。定义一个组件直接放到app.vue中引用(主打的就是一个方便)
我这里接收到消息后使用mitt.js进行各消息分发
<template>
<div></div>
</template>
<script setup lang="ts" name="global-websocket">
import { Session } from '@/utils/storage';
import {computed, onMounted, onUnmounted, ref,watch} from "vue";
import {eventBus} from "@/utils/eventBus"
import other from "@/utils/other";
const props = defineProps({
uri: {
type: String,
},
});
const isLogin=ref<any>()
const worker=ref()
const token = computed(() => {
return Session.getToken();
});
const tenant = computed(() => {
return Session.getTenant();
});
watch(isLogin,(newValue, oldValue) =>{
if(newValue){
initWebSocket();
}
})
onMounted(() => {
// initWebSocket();
if(sessionStorage.getItem('token')){
initWebSocket();
}else{
window.addEventListener('setItem', () => {
isLogin.value = sessionStorage.getItem('token')
});
}
});
onUnmounted(() => {
let conf={
type:"wsClose",
}
worker.value.port.postMessage(JSON.stringify(conf))
});
const initWebSocket = () => {
if (!SharedWorker) {
throw new Error("当前浏览器不支持SharedWorker");
}
// 创建
worker.value = new SharedWorker("../../../public/SharedWorker.js");
// 线程监听消息
worker.value.port.onmessage = (e:any) => {
console.log("接受到消息:", e.data);
sendEventBus(JSON.parse(e.data))
};
let conf={
type:"open",
host:window.location.host,
baseURL:import.meta.env.VITE_API_URL,
uri:other.adaptationUrl(props.uri),
token:token.value,
tenant:tenant.value
}
worker.value.port.postMessage(JSON.stringify(conf))
};
const sendEventBus=(text:any)=>{
switch (text.type){
case "pong":
return;
case "discuss":
eventBus.emit('discuss', text);
break;
case "onlineusers":
eventBus.emit('onlineusers', text);
break;
case "livestart":
eventBus.emit('livestart', text);
break;
case "message_notify":
eventBus.emit('message_notify', text);
break;
}
}
</script>
mitt消息总线的使用
npm install --save mitt
// eventBus.ts
import createEventBus from 'mitt';
export const eventBus = createEventBus();
使用
import {eventBus} from "@/utils/eventBus"
//发送消息
eventBus.emit('discuss', text);
//监听消息
eventBus.on('discuss', (data) => {
console.log(data)
});
本文借鉴:https://blog.csdn.net/jinke0010/article/details/124248321