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

相关推荐
多多*34 分钟前
Spring之Bean的初始化 Bean的生命周期 全站式解析
java·开发语言·前端·数据库·后端·spring·servlet
linweidong39 分钟前
在企业级应用中,你如何构建一个全面的前端测试策略,包括单元测试、集成测试、端到端测试
前端·selenium·单元测试·集成测试·前端面试·mocha·前端面经
满怀10151 小时前
【HTML 全栈进阶】从语义化到现代 Web 开发实战
前端·html
东锋1.31 小时前
前端动画库 Anime.js 的V4 版本,兼容 Vue、React
前端·javascript·vue.js
满怀10151 小时前
【Flask全栈开发指南】从零构建企业级Web应用
前端·python·flask·后端开发·全栈开发
小杨升级打怪中2 小时前
前端面经-webpack篇--定义、配置、构建流程、 Loader、Tree Shaking、懒加载与预加载、代码分割、 Plugin 机制
前端·webpack·node.js
Yvonne爱编码2 小时前
CSS- 4.4 固定定位(fixed)& 咖啡售卖官网实例
前端·css·html·状态模式·hbuilder
SuperherRo2 小时前
Web开发-JavaEE应用&SpringBoot栈&SnakeYaml反序列化链&JAR&WAR&构建打包
前端·java-ee·jar·反序列化·war·snakeyaml
大帅不是我3 小时前
Python多进程编程执行任务
java·前端·python
前端怎么个事3 小时前
框架的源码理解——V3中的ref和reactive
前端·javascript·vue.js