本封装方式只是真对目前自己所在公司当下业务需求,后期可能会修改,本文仅仅只作为学习记录。
使用方式
主要使用的方式有4种,建立连接 io()、关闭连接 WS.close()、发送信息使用 WS.send() / io().send() 、监听 WS.on() / io().on()
import { WSMessageTypeMap, WS, io } from '@/utils/webSocket'
// ---------------------建立连接 -----------------------------
io({...})
// ---------------------关闭连接 -----------------------------
WS.close() 或者 WS.close(()=>{}) // 支持回调
// ---------------------发送 -----------------------------
WS.send({
type:WSMessageTypeMap['订单状态通知'], // type:必填
data?:{}
},function (){
//回调方法
})
// ---------------------监听 -----------------------------
WS.on(WSMessageTypeMap['订单状态通知'],function (){
//回调方法
})
// 或着
WS.on(WSMessageTypeMap['订单状态通知'],[function (){
//回调方法
}])
封装过程
WebSocketClient
用于创建 webSocket 实例,并建立连接。
HeartBeat
用于心跳检测,主动向后端发送消息,当一定时间内没有收到后端恢复,就会断开重连。
最终结果:
// 业务类型:0-心跳检测;1-订单状态通知;2-接驾路线通知;3-服务中订单查询。。。。后期会组件扩展增多
export const WSMessageTypeMap = {
HeartBeat: 0,
订单状态通知: 1,
接驾路线通知: 2,
服务中订单查询: 3,
司机到达时间: 4,
}
const replaceUrl = (url, opts) => {
url += '/websocket?'
for (const key in opts) {
// HeartBeatTimeout 心跳时间不处理
if (opts.hasOwnProperty(key) && key != 'HeartBeatTimeout') {
url = url + `${key}=${opts[key]}&`
}
}
// console.log('🚀 ~ file: webSocket.js ~ line 10 ~ replaceUrl ~ url', url)
// TODO: 需要支持wss,ws明文传输在生产环境太危险了
url = url
.slice(0, -1)
.split('//')
.join('/')
.replace('https:/', 'wss://')
.replace('http:/', 'ws://')
return url
}
/**
* HeartBeat 心跳检测,防止websocket中途断开,断开后重连
* @param {object} websocketClient websocketClient实例
* @param {number} timeout 心跳时间,默认10s
*/
class HeartBeat {
_timeout = 10000
_timeoutObj = null
_serverTimeoutObj = null
_websocketClient = null
constructor(websocketClient, timeout) {
this._websocketClient = websocketClient
this._timeout = timeout || this._timeout
this.reset()
this.start()
}
reset() {
clearTimeout(this._timeoutObj)
clearTimeout(this._serverTimeoutObj)
return this
}
start() {
const self = this
this._timeoutObj && clearTimeout(this._timeoutObj)
this._serverTimeoutObj && clearTimeout(this._serverTimeoutObj)
this._timeoutObj = setTimeout(function () {
// 这里发送一个心跳,后端收到后,返回一个心跳消息,
// onmessage拿到返回的心跳就说明连接正常
self._websocketClient.send({ type: WSMessageTypeMap['HeartBeat'] })
self._serverTimeoutObj = setTimeout(function () {
// console.log('🚀 ~ file: webSocket.js ~ line 61 ~ HeartBeat ~ setTimeout', '主动关闭连接')
// 如果超过一定时间还没重置,说明后端主动断开了
self._websocketClient.close() // 如果onclose会执行reconnect,我们执行close()就行了.如果直接执行 reconnect 会触发onclose导致重连两次
}, self._timeout)
}, self._timeout)
}
}
/**
* @param {string} url 指定创建连接的地址,默认为 process.env.BASE_API
* @param {object} opts 指定配置项
*/
export class WebSocketClient {
_websocket
_token
_path // 路径
_opts
_readyState
_isConnected // 是否连接
openList = [] // 连接成功回调队列
wsCallbackMap = new Map() // 回调Map
closewebsocketCallback = () => {} // websocket 关闭回调函数
_lockReconnect = false // 避免重复连接
_tt
HeartBeat = null // 心跳检测对象
constructor(opts, url) {
if (WS) return WS
this._opts = opts
this.createWebSocket(url)
}
createWebSocket(url) {
if (this._websocket) {
return this._websocket
}
this._path =
this._path || replaceUrl(url || process.env.BASE_API, this._opts)
try {
if ('WebSocket' in window) {
this._websocket = new WebSocket(this._path)
} else if ('MozWebSocket' in window) {
// Note: 新版的FirFox已经没有这个构造函数了 99.0 (64 位)
// eslint-disable-next-line no-undef
this._websocket = new MozWebSocket(this._path)
}
this.init()
} catch (e) {
this.reconnect()
// eslint-disable-next-line no-console
console.error('createWebSocket error', e)
}
}
/**
* 初始化
*/
init() {
this.HeartBeat =
this.HeartBeat || new HeartBeat(this, this._opts.HeartBeatTimeout)
// websocket 连接成功
this._websocket.onopen = () => {
// console.log('🚀 ~ file: webSocket.js ~ line 104 ~ WebSocketClient ~ init ~ onopen',)
this._isConnected = true
// 执行连接成功的回调
const f = this.openList.shift()
typeof f === 'function' && f()
}
this._websocket.onmessage = (event) => {
const data = JSON.parse(event.data)
if (
process.env.NODE_ENV === 'development' &&
data.type !== WSMessageTypeMap['HeartBeat']
) {
// eslint-disable-next-line no-console
console.log(
'🚀 ~ file: webSocket.js ~ line 111 ~ WebSocketClient ~ init ~ onmessage',
data
)
}
// 重启心跳检测
this.HeartBeat.reset().start()
// 执行所有回调
const wsCallbackMap = this.wsCallbackMap.get(data.type)
wsCallbackMap && wsCallbackMap.forEach((callback) => callback(data.data))
}
this._websocket.onclose = () => {
// console.log('🚀 ~ file: webSocket.js ~ line 113 ~ WebSocketClient ~ init ~ onclose',)
this._isConnected = false
this.HeartBeat.reset()
this.reconnect()
}
this._websocket.onerror = () => {
// console.log('🚀 ~ file: webSocket.js ~ line 118 ~ WebSocketClient ~ init ~ onerror')
this._isConnected = false
this.reconnect()
}
// 其他异常情况处理
// 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
this.close()
}
// 处理手机手机熄屏后会中断
document.addEventListener('visibilitychange', () => {
let hiddenTime
if (document.visibilityState == 'hidden') {
// 记录页面隐藏时间
hiddenTime = new Date().getTime()
} else {
const visibleTime = new Date().getTime()
// 页面再次可见的时间-隐藏时间>10S,重连
if ((visibleTime - hiddenTime) / 1000 > 10) {
// 主动关闭连接
this.close()
// 1.5S后重连 因为断开需要时间,防止连接早已关闭了
setTimeout(() => {
this.reconnect()
}, 1500)
} else {
//
}
}
})
}
/**
* 收集回调函数,当收到服务器消息时,会执行对应的回调函数
* @param {*} callback 回调函数,可以是一个回调函数队列数组
* @param {*} method 回调队列名称,对应前后的交互数据的 type
*/
on(method, callback) {
const tm = this.wsCallbackMap.get(method)
if (tm) {
Array.isArray(callback) ? tm.push(...callback) : tm.push(callback)
} else {
const t = Array.isArray(callback) ? [...callback] : [callback]
this.wsCallbackMap.set(method, t)
}
}
/**
* 删除回调函数
* @param {*} method 回调队列名称,对应前后的交互数据的 type
*/
removeOn(method) {
this.wsCallbackMap.delete(method)
}
/**
* 主动向后端发送消息
* @param {*} data 发送的数据 格式必须为: {type:1,data:{}},type为必须的字段,因为是根据type来收集回调函数
* @param {Functon} callback 发送成功后后端返回数据执行回调函数
* data : {
* type: 0, // WSMessageTypeMap[key]
* data: {} // 需要发送的数据
* }
*/
send(data, callback = () => { }) {
const cheapFunction = () => {
data.data = { ...data.data }
if (data.type !== WSMessageTypeMap['HeartBeat']) {
this.removeOn(`send_${data.type}`) // 删除回调函数,必须先删除,否则会导致多次执行
this.on(`send_${data.type}`, callback)
}
this._websocket.send(JSON.stringify(data))
if (data.type !== WSMessageTypeMap['HeartBeat']) {
// 重启心跳检测 方式发送不是心态检测内容时候,没有收到信息,导致心跳检测失效
this.HeartBeat.reset()
}
}
// 如果连接成功,直接发送
if (this._isConnected) {
cheapFunction()
} else {
// 添加在openList 延迟执行
this.openList.push(() => {
setTimeout(() => {
cheapFunction()
}, 1000)
})
}
}
/**
* 关闭连接
*/
close(closewebsocketCallback) {
this.closewebsocketCallback =
closewebsocketCallback || this.closewebsocketCallback
this._websocket.close()
this._lockReconnect = true
this._isConnected = false
this._path = undefined
this._websocket = null
this.closewebsocketCallback()
}
/**
* 重连
* @returns
*/
reconnect() {
// console.log('🚀 ~ file: webSocket.js ~ line 185 ~ WebSocketClient ~ reconnect ~ reconnect')
if (this._isConnected) return
this._lockReconnect = true
this.tt && clearTimeout(this.tt)
// 没连接上会一直重连,设置延迟避免请求过多
this.tt = setTimeout(() => {
// console.log('🚀 ~ file: webSocket.js ~ line 194 ~ WebSocketClient ~ this.tt=setTimeout ~ setTimeout',)
this._lockReconnect = false
this._websocket = null
this.createWebSocket()
}, 4000)
}
}
let WS = null
/**
* 默认创建websocketClient 方法
* @param {*} opts 配置对象
*/
const io = (opts) => {
// 一定要判断是否已经创建过WebSocketClient,否则会导致重复创建
if (WS) return WS
const userInfoEtravel = {
...JSON.parse(sessionStorage.getItem('userInfoEtravel')),
}
opts = {
token: userInfoEtravel.token,
ID: userInfoEtravel.staffId,
HeartBeatTimeout: 3000,
...opts,
}
WS = new WebSocketClient(opts)
return WS
}
/**
*
* @param {Object} WS WS 为 WebSocketClient 实例,可以通过创建 io() 获取
* @param {Function} io io 为 默认 创建 WebSocketClient 实例的方法,可自定义自己的创建方法
*
*
// ---------------------建立连接 -----------------------------
io({...})
// ---------------------关闭连接 -----------------------------
WS.close() 或者 WS.close(()=>{}) // 支持回调
// ---------------------发送 -----------------------------
WS.send({
type:WSMessageTypeMap['订单状态通知'], // type:必填
data?:{}
},function (){
//回调方法
})
// ---------------------监听 -----------------------------
WS.on(WSMessageTypeMap['订单状态通知'],function (){
//回调方法
})
// 或着
WS.on(WSMessageTypeMap['订单状态通知'],[function (){
//回调方法
}])
*
*/
export { WS, io }