在vue3中封装使用WebSocket

慈云数据 2024-05-01 技术支持 105 0

上篇文章记录了如何在日常开发过程中引入并使用webSocket连接,但是在后续的开发过程中发现之前的写法有点问题,比如说多次引用连接会共用一个心跳,如果一个连接关掉了,后续其他的连接可能被一起关掉等等的bug。

在vue3中封装使用WebSocket
(图片来源网络,侵删)

所以在这篇文章里针对上篇文章提供的方法进行改进,同时提供兼容vue3写法。

一、创建 WebSocket 类

class Socket {
    constructor(url, opts = {}) {
        this.url = url;
        this.ws = null;
        this.opts = {
            heartbeatInterval: 30000, // 默认30秒
            reconnectInterval: 5000, // 默认5秒
            maxReconnectAttempts: 5, // 默认尝试重连5次
            ...opts
        };
        this.reconnectAttempts = 0;
        this.listeners = {};
        this.init();
    }
    init() {
        this.ws = new WebSocket(this.url);
        this.ws.onopen = this.onOpen.bind(this);
        this.ws.onmessage = this.onMessage.bind(this);
        this.ws.onerror = this.onError.bind(this);
        this.ws.onclose = this.onClose.bind(this);
    }
    onOpen(event) {
        console.log('WebSocket opened:', event);
        this.reconnectAttempts = 0; // 重置重连次数
        this.startHeartbeat();
        this.emit('open', event);
    }
    onMessage(event) {
        console.log('WebSocket message received:', event.data);
        this.emit('message', event.data);
    }
    onError(event) {
        console.error('WebSocket error:', event);
        this.emit('error', event);
    }
    onClose(event) {
        console.log('WebSocket closed:', event);
        this.stopHeartbeat();
        this.emit('close', event);
        if (this.reconnectAttempts  {
                this.reconnectAttempts++;
                this.init();
            }, this.opts.reconnectInterval);
        }
    }
    // 发送心跳
    startHeartbeat() {
        this.heartbeatInterval = setInterval(() => {
            if (this.ws.readyState === WebSocket.OPEN) {
                this.ws.send('ping'); // 可以修改为你的心跳消息格式
            }
        }, this.opts.heartbeatInterval);
    }
    // 停止心跳
    stopHeartbeat() {
        if (this.heartbeatInterval) {
            clearInterval(this.heartbeatInterval);
            this.heartbeatInterval = null;
        }
    }
    send(data) {
        if (this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(data);
        } else {
            console.error('WebSocket is not open. Cannot send:', data);
        }
    }
    on(event, callback) {
        if (!this.listeners[event]) {
            this.listeners[event] = [];
        }
        this.listeners[event].push(callback);
    }
    off(event, callback) {
        if (!this.listeners[event]) return;
        const index = this.listeners[event].indexOf(callback);
        if (index !== -1) {
            this.listeners[event].splice(index, 1);
        }
    }
    emit(event, data) {
        if (this.listeners[event]) {
            this.listeners[event].forEach(callback => callback(data));
        }
    }
}
export default Socket;

我们首先定义一个 Socket 类,该类会负责与 WebSocket 服务器建立连接、发送和接收数据、以及管理心跳和重连逻辑。 

在vue3中封装使用WebSocket
(图片来源网络,侵删)

在你的Vue组件中使用这个类时,可以这样注册事件:

import Socket from './socket.js';
export default {
    data() {
        return {
            socket: null
        };
    },
    created() {
        this.socket = new Socket('ws://your-websocket-url');
        this.socket.on('open', event => {
            console.log("Connected to server", event);
        });
        this.socket.on('message', data => {
            console.log("Received data:", data);
        });
        this.socket.on('error', error => {
            console.error("WebSocket Error:", error);
        });
        this.socket.on('close', event => {
            console.log("Connection closed", event);
        });
    },
    beforeDestroy() {
        // 取消所有事件监听器
        this.socket.off('open');
        this.socket.off('message');
        this.socket.off('error');
        this.socket.off('close');
    },
    methods: {
        sendToServer(data) {
            this.socket.send(data);
        }
    }
}

二、使用 Vue 3 的 Composition API

为了在 Vue 3 中更好地使用上述的 Socket 类,我们将其封装为一个 composable 函数,这样可以轻松地在任何 Vue 组件中使用 WebSocket。

import { ref, onUnmounted } from 'vue';
interface SocketOptions {
  heartbeatInterval?: number;
  reconnectInterval?: number;
  maxReconnectAttempts?: number;
}
class Socket {
  url: string;
  ws: WebSocket | null = null;
  opts: SocketOptions;
  reconnectAttempts: number = 0;
  listeners: { [key: string]: Function[] } = {};
  heartbeatInterval: number | null = null;
  constructor(url: string, opts: SocketOptions = {}) {
    this.url = url;
    this.opts = {
      heartbeatInterval: 30000,
      reconnectInterval: 5000,
      maxReconnectAttempts: 5,
      ...opts
    };
    this.init();
  }
  init() {
    this.ws = new WebSocket(this.url);
    this.ws.onopen = this.onOpen.bind(this);
    this.ws.onmessage = this.onMessage.bind(this);
    this.ws.onerror = this.onError.bind(this);
    this.ws.onclose = this.onClose.bind(this);
  }
  onOpen(event: Event) {
    console.log('WebSocket opened:', event);
    this.reconnectAttempts = 0;
    this.startHeartbeat();
    this.emit('open', event);
  }
  onMessage(event: MessageEvent) {
    console.log('WebSocket message received:', event.data);
    this.emit('message', event.data);
  }
  onError(event: Event) {
    console.error('WebSocket error:', event);
    this.emit('error', event);
  }
  onClose(event: CloseEvent) {
    console.log('WebSocket closed:', event);
    this.stopHeartbeat();
    this.emit('close', event);
    
    if (this.reconnectAttempts  {
        this.reconnectAttempts++;
        this.init();
      }, this.opts.reconnectInterval);
    }
  }
  startHeartbeat() {
    if (!this.opts.heartbeatInterval) return;
    
    this.heartbeatInterval = window.setInterval(() => {
      if (this.ws?.readyState === WebSocket.OPEN) {
        this.ws.send('ping');
      }
    }, this.opts.heartbeatInterval);
  }
  stopHeartbeat() {
    if (this.heartbeatInterval) {
      clearInterval(this.heartbeatInterval);
      this.heartbeatInterval = null;
    }
  }
  send(data: string) {
    if (this.ws?.readyState === WebSocket.OPEN) {
      this.ws.send(data);
    } else {
      console.error('WebSocket is not open. Cannot send:', data);
    }
  }
  on(event: string, callback: Function) {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event].push(callback);
  }
  off(event: string) {
    if (this.listeners[event]) {
      delete this.listeners[event];
    }
  }
  emit(event: string, data: any) {
    this.listeners[event]?.forEach(callback => callback(data));
  }
}
export function useSocket(url: string, opts?: SocketOptions) {
  const socket = new Socket(url, opts);
  onUnmounted(() => {
    socket.off('open');
    socket.off('message');
    socket.off('error');
    socket.off('close');
  });
  return {
    socket,
    send: socket.send.bind(socket),
    on: socket.on.bind(socket),
    off: socket.off.bind(socket)
  };
}

在组件中使用:

import { defineComponent } from 'vue';
import { useSocket } from './useSocket';
export default defineComponent({
  name: 'YourComponent',
  setup() {
    const { socket, send, on, off } = useSocket('ws://your-websocket-url');
    on('open', event => {
      console.log("Connected to server", event);
    });
    on('message', data => {
      console.log("Received data:", data);
    });
    on('error', error => {
      console.error("WebSocket Error:", error);
    });
    on('close', event => {
      console.log("Connection closed", event);
    });
    return {
      send
    };
  }
});

三、总结

以上是具体实现方案,在后续开发过程中如果有更好的写法,也会更新本文。

微信扫一扫加客服

微信扫一扫加客服

点击启动AI问答
Draggable Icon