我正在参加「掘金·启航计划」
说明
此教程针对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)
})
结尾
欢迎大家指出错误或者提出意见和建议 ~