如何在vue3中使用属于自己的WebSocket,绝了~

6,558 阅读6分钟

我正在参加「掘金·启航计划」

说明

此教程针对typescript,提供断线自动重连,断线数据重发,自动心跳,自定义消息发送机制

示例仓库:在vue3+typescript-websocket示例

介绍

在前端开发中不免用到websocket进行和后端通讯,刚开始时是简单的使用监听即可完成需求:

import WebSocket from 'ws';

// WebSocket连接的URL
const url = 'ws://example.com/socket';

// 创建WebSocket实例
const socket = new WebSocket(url);

// 监听连接成功事件
socket.on('open', () => {
  console.log('WebSocket连接已打开');

  // 发送消息
  socket.send('Hello, server!');
});

// 监听接收消息事件
socket.on('message', (data: WebSocket.Data) => {
  console.log('接收到消息:', data);

  // 关闭连接
  socket.close();
});

// 监听连接关闭事件
socket.on('close', () => {
  console.log('WebSocket连接已关闭');
});

// 监听连接错误事件
socket.on('error', (error: Error) => {
  console.error('WebSocket错误:', error);
});

后面随着应用的复杂程度和应用需求,慢慢的加上了心跳、重连等功能,加上vue3的一些功能,于是封装了npm仓库: tools-vue3,这篇文章主要介绍其中的Websocket部分。

Websocket封装

一共是6个文件,1个类型提示加上5个功能实现

类型提示:websocket.d.ts

功能实现:WebSocketBean.ts、WebSocketHeart.ts、WebSocketReconnect.ts、WebSocketSend.ts、WebSocketEnum.ts

WebSocketBean.ts文件

用于使用时的入口文件,调用了心跳,重连,消息管理业务代码,对外提供了生命周期调用、消息发送等方法

import { IWebSocketBean, IWebSocketBeanParam, IWebSocketReconnect, IWebSocketSend } from './websocket'
import WebSocketHeart from './WebSocketHeart'
import WebSocketReconnect from './WebSocketReconnect'
import WebSocketSend from './WebSocketSend'
import { WebSocketStatusEnum } from './WebSocketEnum'

/**
 * WebSocket封装类
 * @param 封装了心跳机制 、重连机制
 */
export default class WebSocketBean implements IWebSocketBean {
    status: WebSocketStatusEnum = null as any
    websocket: WebSocket = null as any
    heart: WebSocketHeart = null as any
    reconnect: IWebSocketReconnect = null as any
    sendObj: IWebSocketSend = null as any
    param: IWebSocketBeanParam

    constructor(param: IWebSocketBeanParam) {
        this.param = param
    }

    onopen = async () => {
        //开启心跳
        this.heart.start()

        //通知连接成功或重连成功
        this.reconnect.stop()

        //调用生命周期
        if (this.param.onopen) await this.param.onopen()

        //修改状态为已连接
        this.status = WebSocketStatusEnum.open

        //通知发送数据
        this.sendObj.onopen()
    }

    onmessage = (ev: MessageEvent<any>) => {
        //调用生命周期
        if (this.param.onmessage) this.param.onmessage(ev)

        this.heart.onmessage(ev.data)
    }

    onerror = () => {
        //调用生命周期
        if (this.param.onerror) this.param.onerror()
        //销毁对象
        this.close()
        //开始重连
        this.reconnect.start()
    }

    start = (param?: IWebSocketBeanParam) => {
        //如果已经创建先关闭
        this.close()

        //使用新配置或者老配置
        if (param) this.param = param
        else param = this.param

        //创建连接
        this.websocket = new WebSocket(param.url)

        //修改状态为加载中
        this.status = WebSocketStatusEnum.load

        //绑定连接成功事件
        this.websocket.onopen = this.onopen
        //绑定消息接收事件
        this.websocket.onmessage = this.onmessage
        //绑定连接异常事件
        this.websocket.onerror = this.onerror
        //绑定连接关闭事件
        this.websocket.onclose = this.onerror

        //创建心跳
        this.heart = new WebSocketHeart(this)

        //创建重连,如果存在则跳过
        if (this.reconnect === null) this.reconnect = new WebSocketReconnect(this)

        //创建发送数据管理,如果存在则跳过
        if (this.sendObj === null) this.sendObj = new WebSocketSend(this)

        //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
        window.addEventListener('beforeunload', this.dispose)
    }

    /**
     * 发送数据
     * @param data 数据对象,Object、Array、String
     */
    send(data: any, resend: boolean = false) {
        return this.sendObj?.send(data, resend)
    }

    /**
     * 销毁需要重发的数据信息
     * @param sendId
     */
    offsend = (sendId: string) => {
        this.sendObj?.offsend(sendId)
    }

    /**
     * 关闭socket,销毁绑定事件、心跳事件、窗口关闭事件,修改状态为已关闭
     */
    close = () => {
        if (this.websocket === null) return
        window.removeEventListener('beforeunload', this.dispose)
        //销毁绑定事件,关闭socket
        if (this.websocket) {
            this.websocket.onerror = null
            this.websocket.onmessage = null
            this.websocket.onclose = null
            this.websocket.onopen = null
            this.websocket.close()
            this.websocket = null as any
        }
        //销毁心跳事件
        if (this.heart) {
            this.heart.stop()
            this.heart = null as any
        }

        //修改状态为已关闭
        this.status = WebSocketStatusEnum.close
    }

    /**
     * 销毁所有对象
     */
    dispose = () => {
        this.close()
        if (this.reconnect) {
            this.reconnect.stop()
            this.reconnect = null as any
        }
        if (this.sendObj) {
            this.sendObj.clear()
            this.sendObj = null as any
        }
    }
}

WebSocketHeart.ts文件

心跳服务,配置发送字符串和接收字符串以进行心跳反馈,还有心跳时间间隔、心跳失败次数可配置


import { IWebSocketHeart, IWebSocketBean } from './websocket'

/**
 * WebSocket心跳机制
 */
export default class WebSocketHeart implements IWebSocketHeart {
    websocketbean: IWebSocketBean
    heartSend: string
    heartGet: string
    heartGapTime: number
    failNum: number = 0
    heartFailNum: number

    constructor(websocketbean: IWebSocketBean) {
        this.websocketbean = websocketbean
        this.heartSend = this.websocketbean.param.heartSend ?? 'heartSend'
        this.heartGet = this.websocketbean.param.heartGet ?? 'heartGet'
        this.heartGapTime = this.websocketbean.param.heartGapTime ?? 30000
        this.heartFailNum = this.websocketbean.param.heartFailNum ?? 10
    }

    timer: number = null as any

    start = () => {
        if (this.timer !== null) return
        this.failNum = 0
        this.timer = setInterval(() => {
            if (this.failNum >= this.heartFailNum) {
                this.stop()
                this.websocketbean.onerror()
                return
            }
            this.websocketbean.send(this.heartSend)
            this.failNum++
        }, this.heartGapTime) as any
    }

    stop = () => {
        clearInterval(this.timer)
        this.timer = null as any
    }

    onmessage = (ev: any) => {
        const messagePrefix = this.websocketbean.param.messagePrefix ?? ''
        const messageSuffix = this.websocketbean.param.messageSuffix ?? ''
        const heartGetMessage = messagePrefix + this.heartGet + messageSuffix
        if (ev === heartGetMessage) this.failNum = 0
    }
}


WebSocketReconnect.ts文件

重连服务,配置是否开启自动重连服务,最大重连次数,重连时间间隔


import { IWebSocketReconnect, IWebSocketBean } from './websocket'

/**
 * WebSocket重连机制和重连重发数据机制
 */
export default class WebSocketReconnect implements IWebSocketReconnect {
    /**
     * 开启状态
     */
    status: boolean

    /**
     * WebSocketBean对象
     */
    websocketbean: IWebSocketBean

    /**
     * 当前重连次数
     */
    num: number = 0

    /**
     * 最大重连次数
     */
    reconnectMaxNum: number = 10

    /**
     * 重连间隔时间
     */
    reconnectGapTime: number = 30000

    constructor(websocketbean: IWebSocketBean) {
        this.websocketbean = websocketbean
        this.status = websocketbean.param.needReconnect ?? false
        this.reconnectMaxNum = this.websocketbean.param.reconnectMaxNum ?? 10
        this.reconnectGapTime = this.websocketbean.param.reconnectGapTime ?? 30000
    }

    timer: number = null as any

    /**
     * 开始尝试重连
     */
    start = () => {
        if (!this.status) return
        if (this.timer !== null) return
        this.num = 0
        if (this.websocketbean.param.onreconnect) this.websocketbean.param.onreconnect()
        this.timer = setInterval(() => {
            if (this.num >= this.reconnectMaxNum) {
                if (this.websocketbean.param.onFailReconnect) this.websocketbean.param.onFailReconnect()
                this.stop()
                return
            }
            this.websocketbean.start()
            this.num++
        }, this.reconnectGapTime) as any
    }

    /**
     * 停止重连
     */
    stop = () => {
        if (!this.status) return
        clearInterval(this.timer)
        this.timer = null as any
    }
}


WebSocketSend.ts文件

数据发送管理,可配置数据发送的统一前缀或者后缀,用于处理粘包拆包啥的


import { IWebSocketBean, IWebSocketSend } from './websocket'
import { WebSocketStatusEnum } from './WebSocketEnum'

const isObject = (val: any): any => val !== null && typeof val === 'object'

/**
 * WebSocket数据发送管理
 */
export default class WebSocketSend implements IWebSocketSend {
    websocketbean: IWebSocketBean

    sendPrefix: string
    sendSuffix: string

    constructor(websocketbean: IWebSocketBean) {
        this.websocketbean = websocketbean
        this.sendPrefix = this.websocketbean.param.sendPrefix ?? ''
        this.sendSuffix = this.websocketbean.param.sendSuffix ?? ''
    }

    /**
     * 临时发送管理对象
     */
    sendTemp: { tag: string; data: any; resend: boolean; sendId?: string }[] = []

    /**
     * 重新发送id
     */
    sendId: number = 1000

    /**
     * 获取重新发送id
     * @returns
     */
    getSendId = () => {
        this.sendId++
        return this.sendId + ''
    }

    /**
     * 缓存数据标识
     */
    tag = '___senTemp'

    /**
     * 重新发送的数据管理
     */
    sendMap: { [key: string]: any } = {}

    /**
     * 发送数据
     * @param data 数据对象,Object、Array、String
     */
    send(data: any, resend: boolean = false) {
        if (this.websocketbean.status === WebSocketStatusEnum.open) {
            let sendId: string = null as any

            //先判断是不是缓存待发送的数据,如果是取出待发送的数据和状态
            if (isObject(data)) {
                if (data.tag === this.tag) {
                    resend = data.resend
                    //如果resend是true,sendId一定存在
                    if (resend) sendId = data.sendId
                    data = data.data
                }
            }

            //如果需要重发就保存起来
            if (resend) {
                if (sendId === null) sendId = this.getSendId()
                this.sendMap[sendId] = data
            }

            //判断是不是对象或者数组,转换为字符串
            if (isObject(data) || Array.isArray(data)) {
                data = JSON.stringify(data)
            }

            //发送数据
            this.websocketbean.websocket.send(this.sendPrefix + data + this.sendSuffix)

            //如果是需要重发的返回sendId
            return resend ? sendId : true
        } else {
            let sendId: string = null as any

            if (isObject(data)) {
                //说明是缓存待发送数据,不做处理
                if (data.tag === this.tag) return false
            }

            //未连接上时存入临时缓存,连上后发送
            const sendTempItem: any = {
                tag: this.tag,
                data,
                resend
            }

            if (resend) {
                sendId = this.getSendId()
                sendTempItem.sendId = sendId
            }
            this.sendTemp.push(sendTempItem)
            return resend ? sendId : false
        }
    }

    /**
     * 销毁需要重发的数据信息
     * @param sendId
     */
    offsend = (sendId: string) => {
        this.sendMap[sendId] = undefined
        delete this.sendMap[sendId]
    }

    /**
     * 通知连接打开
     */
    onopen = () => {
        //处理重发数据
        Object.keys(this.sendMap).forEach((key) => {
            if (this.sendMap[key] !== undefined) this.send(this.sendMap[key])
        })

        //处理临时数据
        for (let i = this.sendTemp.length - 1; i >= 0; i--) {
            const item = this.sendTemp[i]
            const sendStatus = this.send(item)
            if (sendStatus !== false) this.sendTemp.splice(i, 1)
        }
    }

    /**
     * 清空所有缓存数据
     */
    clear = () => {
        this.sendMap = {}
        this.sendTemp = []
    }
}


WebSocketStatusEnum.ts文件

枚举文件,对连接状态进行管理


export enum WebSocketStatusEnum {
    /**
     * 创建中
     */
    load,
    /**
     * 已连接
     */
    open,
    /**
     * 已关闭
     */
    close
}


使用示例

示例仓库:在vue3+typescript-websocket示例

上面介绍了具体代码实现,这里是直接引用的npm仓库,当然也可以直接把上的文件创建到项目中然后直接使用,不需要引用npm仓库,

  • 安装tools-vue3 -> 使用pnpm i tools-vue3或者npm i tools-vue3或者cnpm i tools-vue3等都可以
  • 创建 WSUtil.ts文件
  • 内容:
import { WebSocketBean } from 'tools-vue3'
export default class WSUtil {
    static ws: WebSocketBean
    static async init() {
        const sendSuffix = ''
        
        //初始化websokcet对象
        this.ws = new WebSocketBean({
            url: 'ws://192.168.1.66:8801/ws',
            needReconnect: true,
            reconnectGapTime: 3000,
            onerror: () => {
                console.log('断开')
            },
            sendSuffix,
            messageSuffix: sendSuffix,
            heartSend: '~',
            heartGet: '~',
            heartGapTime: 3000,
            onmessage: (data) => {
            	//在这里写消息处理逻辑
                console.log(data.data)
                const sp = data.data.split(sendSuffix).filter((el: string) => el.length > 0)
                console.log(sp)
            }
        })
        //建立连接
        this.ws.start()
    }
}

  • 发送数据
WSUtil.ws.send('data')
  • 主动断开
WSUtil.ws.dispose()
  • 消息处理

对消息处理一般使用事件总线去做,我一般使用我写的另一个npm仓库:tools-javascript,提供了CEvent.on和CEvent.emit去做即可

        //安装tools-javascript
        npm i tools-javascript
        //在main.ts中引入tools-javascript
        import 'tools-javascript'
        //在WSUtil.ts的onmessage方法中
        onmessage: (data) => { 
            //在这里写消息处理逻辑 
            console.log(data.data) 
            const sp = data.data.split(sendSuffix).filter((el: string) => el.length > 0) 
            console.log(sp) 
            //这里sp的数据为['{\"code\":\"getData\",\"data\":\"test\"}']
            sp.forEach(item=>{
                const jsonData = JSON.parse(item)
                //事件触发
                CEvent.emit(jsonData.code,jsonData.data)
            })
        }


        //在任意文件或者页面中
        CEvent.on("getData",(data)=>{
            //在onmessage触发后,这里应该打印test字符串
            console.log(data)
        })

结尾

欢迎大家指出错误或者提出意见和建议 ~