vue-使用Worker实现多标签页共享一个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() 方法来手动开启端口。

js 复制代码
// main.js(主线程)
const myWorker = new SharedWorker('./sharedWorker.js');

myWorker.port.start(); // 开启端口

myWorker.port.addEventListener('message', msg => {
    console.log(msg.data);
})

但是,如果采用 onmessage 方法,则默认开启端口,不需要再手动调用SharedWorker.port.start()方法

js 复制代码
// main.js(主线程)
const myWorker = new SharedWorker('./sharedWorker.js');

myWorker.port.onmessage = msg => {
    console.log(msg.data);
};

SharedWorker 标识与独占

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

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

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

  • 改变url这个好理解
  • 改变文档源
bash 复制代码
demo中我又创建了一个page3.html
和另一个SharedWorker2.js
// 创建
page3与page1中唯一不同的就是引用了SharedWorker2.js
const worker = new SharedWorker("./SharedWorker2.js");

改变名字

bash 复制代码
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

bash 复制代码
// 记个数
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

bash 复制代码
<!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

bash 复制代码
<!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中

js 复制代码
// 记个数
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进行各消息分发

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

ts 复制代码
// eventBus.ts
import createEventBus from 'mitt';

export const eventBus = createEventBus();

使用

js 复制代码
import {eventBus} from "@/utils/eventBus"

//发送消息
eventBus.emit('discuss', text);

//监听消息
eventBus.on('discuss', (data) => {
    console.log(data)
  });

本文借鉴:https://blog.csdn.net/jinke0010/article/details/124248321

相关推荐
2501_9159184142 分钟前
iPhone 抓包工具有哪些?多工具对比分析优缺点
websocket·网络协议·tcp/ip·http·网络安全·https·udp
Boilermaker19921 小时前
【Java EE】SpringIoC
前端·数据库·spring
中微子1 小时前
JavaScript 防抖与节流:从原理到实践的完整指南
前端·javascript
天天向上10241 小时前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y1 小时前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁2 小时前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry2 小时前
Fetch 笔记
前端·javascript
拾光拾趣录2 小时前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟2 小时前
vue3,你看setup设计详解,也是个人才
前端
Lefan2 小时前
一文了解什么是Dart
前端·flutter·dart