webSocket 封装

实现大小屏联动

  • 安装插件
js 复制代码
 npm i sockjs-client -D
 npm i stompjs -D
  • 封装 websocket
js 复制代码
import { defineStore } from "pinia";
import SockJS from "sockjs-client/dist/sockjs.min.js";
import Stomp from "stompjs";

// 心跳和重连常量配置
const HEARTBEAT_INTERVAL = 20_000; // 20秒发送一次PING
const PONG_TIMEOUT = 25_000; // 25秒内未收到PONG视为超时
const MAX_RECONNECT_ATTEMPTS = 10; // 最大重连次数
const RECONNECT_EXPONENTIAL_BACKOFF = true; // 使用指数退避算法
export const useWebSocketStore = defineStore("webSocket", {
  state: () => ({
    socket: null,
    stompClient: null,
    headerToken: {},
    loginStatus: false,
    currentIndex: 0,
    messages: [], // 存储从 WebSocket 接收到的消息,
    socketMessage: null,
    // 添加一个标志来表示连接是否已建立
    connectionEstablished: false,
    reconnectAttempts: 0, // 重连次数
    maxReconnectAttempts: 5, // 最大重连次数
    lastPongTime: 0, // 上次收到PONG的时间
  }),
  actions: {
    initSocket() {
      if (this.socket && this.stompClient) {
        console.warn("WebSocket already initialized.");
        return;
      }
      // 环境配置
      const isLocal = ["192.168.10.xxx", "localhost", "172.16.80.xxx"].includes(
        location.hostname
      );
      const baseUrl = isLocal ? "https://xxxxxx" : location.origin;
      //连接服务端提供的通信接口,连接以后才可以订阅广播消息和个人消息
      this.socket = new SockJS(
        `${baseUrl}/xxxxxxx`
      );
      // 获取STOMP子协议的客户端对象
      this.stompClient = Stomp.over(this.socket);
      // 配置心跳参数 (关键修复)
      this.stompClient.heartbeat.outgoing = HEARTBEAT_INTERVAL;
      this.stompClient.heartbeat.incoming = 0;
      // 定义客户端的认证信息,按需求配置
      this.headerToken = {
        Authorization: "",
      };
      // 向服务器发起websocket连接
      this.stompClient.connect(
        this.headerToken,
        (frame) => {
          this.loginStatus = true;
          // 更新连接状态
          this.reconnectAttempts = 0; // 重置重连次数
          this.connectionEstablished = true; // 连接已建立
          this.startHeartbeat(); // 启动心跳
          this.subscribeTopic();
        },
        (err) => {
          // 连接发生错误时的处理函数
          this.stompClient = null;
          this.handleDisconnect();

          // 首次连接失败直接重连
          if (this.reconnectAttempts === 0) {
            this.scheduleReconnect();
          }
        }
      );
      this.socket.onclose = () => {
        this.handleDisconnect();
      };
    },
    // 处理心跳响应
    handlePong() {
      this.lastPongTime = Date.now();
      clearTimeout(this.pongTimeoutTimer);
    },
    // 统一重连调度
    scheduleReconnect() {
      this.reconnectAttempts++;
      // 计算等待时间
      let delay;
      if (RECONNECT_EXPONENTIAL_BACKOFF) {
        delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
      } else {
        delay = 5000; // 固定5秒重连
      }

      console.log(
        `将在 ${delay}ms 后尝试重连 (尝试次数: ${this.reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`
      );

      setTimeout(() => {
        this.cleanup();
        this.initSocket();
      }, delay);
    },
    // 处理断开连接
    handleDisconnect() {
      this.clearTimers();

      // 自动重连逻辑
      if (
        !this.reconnectAttempts ||
        this.reconnectAttempts < MAX_RECONNECT_ATTEMPTS
      ) {
        this.scheduleReconnect();
      } else {
        console.error(
          `已达到最大重连次数(${MAX_RECONNECT_ATTEMPTS}),停止重连`
        );
      }
    },
    // 启动心跳检测
    startHeartbeat() {
      this.clearTimers();
      // 记录初始PONG时间
      this.lastPongTime = Date.now();

      // 定时发送心跳
      this.heartbeatTimer = setInterval(() => {
        if (this.stompClient?.connected) {
          try {
            this.stompClient.send(
              "/xx/xxx",
              this.headerToken,
              JSON.stringify({ type: "ping" })
            );
            // 设置PONG超时检测
            this.pongTimeoutTimer = setTimeout(() => {
              if (Date.now() - this.lastPongTime > PONG_TIMEOUT) {
                console.warn("PONG响应超时,触发重连");
                this.handleDisconnect();
              }
            }, PONG_TIMEOUT);
          } catch (error) {
            console.error("发送心跳失败:", error);
            this.handleDisconnect();
          }
        }
      }, HEARTBEAT_INTERVAL);
    },
    subscribeTopic() {
      if (!this.stompClient) return;
      this.stompClient.subscribe("/xxx/xxx", (broadcast) => {
        this.socketMessage = JSON.parse(broadcast.body);
        if (this.socketMessage?.tpye == "ping") {
          this.handlePong();
        }
      });
    },
    cleanup() {
      if (this.stompClient?.connected) {
        this.stompClient.disconnect();
      }
      this.clearTimers();
      this.stompClient = null;
      this.socket = null;
    },
    clearTimers() {
      clearInterval(this.heartbeatTimer);
      clearTimeout(this.pongTimeoutTimer);
      this.heartbeatTimer = null;
      this.pongTimeoutTimer = null;
    },
    clearMessages() {
      this.messages = [];
      this.socketMessage = null;
    },
    send(method, info) {
      console.log("send", this.stompClient);
      try {
        if (!this.stompClient || !this.loginStatus) {
          // 增加对 loginStatus 的检查
          console.error("WebSocket connection is not established yet.");
          return;
        }
        let param =
          info === null
            ? null
            : typeof info === "string"
            ? info
            : JSON.stringify(info);
        this.stompClient.send(method, this.headerToken, param);
      } catch (e) {
        this.handleDisconnect();
        return;
      }
    },
  },
});
  • vue3 组件中使用
js 复制代码
import { useWebSocketStore } from '@/stores/webSocket'; // 引入WebSocket Store
const webSocketStore = useWebSocketStore(); // 实例化WebSocket Store

// 小屏发送消息
const  websocketApi = ref({
   setCode:'/app/xxxx'
})
let param = {a:1,b:2}
 webSocketStore.send(websocketApi.value.setCode,param)


// 订阅消息,监听消息

/ 监听 messages 数组的变化
watch(
  () => webSocketStore.socketMessage,
  (newVal, oldVal) => {
    console.log(newVal,'bignewVal');
    // 处理对应的业务逻辑

  },
  { immediate: true, deep: true }
)