/**
* @class WebSocketAuto
* @extends EventTarget
* @description WebSocket包装,扩展超时、心跳、重连机制
* @param {Object} options 选项配置
* @param {String} options.url 连接地址
* @param {String | String[]} options.protocols 对应的原生WebSocket参数
* @param {Number} [options.connectTimeOut = 5000] 默认连接超时时间
* @param {Number} [options.baseReConnectTimeOut = 2000] 重连间隔时间
* @param {Number} [options.maxReConnectTimeOut = 60000] 重连间隔时间
* @param {Number} options.reConnectRepeatLimit 重连次数限制,不设置则不限制
* @param {Boolean} [options.heartCheckEnable = false] 默认不开启心跳检测
* @param {Number} [options.heartCheckTimeOut = 10000] 心跳检测频率
* @param {String | Number} [options.heartCheckPing = 'ping'] 心跳发送包内容
* @param {String | Number} [options.heartCheckPong = 'pong'] 心跳相应包内容
* @param {Number} [options.heartCheckCloseTimeOut = 2000] 心跳检测超时时间
* @param {Boolean} [options.parseMessage = true] 开启消息格式化
* @param {Boolean} [options.debug = false] 开启debug消息打印,需要打开控制台详细级别的输出
* @example
* import { WebSocketAuto } from '@blinkjun/utils';
*
* const ws = new WebSocketAuto({
* url: 'ws://localhost:8080',
* heartCheckEnable: true,
* });
*
* // 发送方法
* try{
* ws.send('hello')
* }catch(e){
*
* }
*
* // 手动关闭
* ws.close()
*
* // 主要监听消息事件
* ws.addEventListener('message', (e) => {
* console.log(e);
* });
*
* // 连接成功,可省略
* ws.addEventListener('open',(e)=>{
* console.log(e)
* })
*
* // 连接关闭,可省略
* ws.addEventListener('close',(e)=>{
* console.log(e)
* })
*
* // 连接错误,可省略
* ws.addEventListener('error',(e)=>{
* console.log(e)
* })
*/
export class WebSocketAuto extends EventTarget {
/**
* @ignore
* @param {Object} options 选项配置
* @param {String} options.url 连接地址
* @param {String | String[]} options.protocols 对应的原生WebSocket参数
* @param {Number} [options.connectTimeOut = 5000] 默认连接超时时间
* @param {Number} [options.baseReConnectTimeOut = 2000] 重连间隔时间
* @param {Number} [options.maxReConnectTimeOut = 60000] 重连间隔时间
* @param {Number} options.reConnectRepeatLimit 重连次数限制,不设置则不限制
* @param {Boolean} [options.heartCheckEnable = false] 默认不开启心跳检测
* @param {Number} [options.heartCheckTimeOut = 10000] 心跳检测频率
* @param {String | Number} [options.heartCheckPing = 'ping'] 心跳发送包内容
* @param {String | Number} [options.heartCheckPong = 'pong'] 心跳相应包内容
* @param {Number} [options.heartCheckCloseTimeOut = 2000] 心跳检测超时时间
* @param {Boolean} [options.parseMessage = true] 开启消息格式化
* @param {Boolean} [options.debug = false] 开启debug消息打印,需要打开控制台详细级别的输出
*/
constructor(options) {
super();
this.url = options.url;
this.protocols = options.protocols;
// 设置连接的超时时间
this.connectTimeOut = options.connectTimeOut || 5000;
// 限制重连次数,默认不限制
this.reConnectTimmer = null;
this.baseReConnectTimeOut = options.baseReConnectTimeOut || 2000;
this.maxReConnectTimeOut = options.maxReConnectTimeOut || 60000;
this.reConnectRepeatLimit = options.reConnectRepeatLimit || null;
this.reConnectRepeatCount = 0;
// 心跳配置
// 是否开启心跳检测,后端需要响应心跳,若后端不响应心跳,会一直重连!
this.heartCheckEnable = options.heartCheckEnable || false;
this.heartCheckTimer = null;
// 心跳频率
this.heartCheckTimeOut = options.heartCheckTimeOut || 10000;
this.heartCheckPing = options.heartCheckPing || "ping";
this.heartCheckPong = options.heartCheckPong || "pong";
// 发送心跳包后等待的时间,超时则重连
this.heartCheckCloseTimer = null;
this.heartCheckCloseTimeOut = options.heartCheckCloseTimeOut || 2000;
// 是否主动关闭
this.activeClose = false;
// 是否格式化消息内容
this.parseMessage = options.parseMessage || true;
// 是否开启debug消息
this.debug = options.debug || false;
// 缓存消息,连接成功时逐条发送
this.messages = [];
this.connect();
}
/**
* @description 连接
* @ignore
*/
connect() {
const ws = (this.ws = new WebSocket(this.url, this.protocols));
// 设置连接超时时间
const connectTimeOutTimer = setTimeout(() => {
ws.close();
this.reConnect();
this.debug &&
console.debug(`websocket connect timeout:`, this.url);
}, this.connectTimeOut);
// 连接成功
ws.addEventListener("open", (event) => {
this.debug && console.debug(`websocket open:`, event);
clearTimeout(connectTimeOutTimer);
// 重置配置
this.reConnectRepeatCount = 0;
this.activeClose = false;
this.dispatchEvent(new CustomEvent("open", { detail: event }));
// 将未连接时缓存下来的消息全部发送出去
while (this.messages.length > 0) {
self.send(this.messages.shift());
}
// 开始心跳检测
if (this.heartCheckEnable) {
this.heartCheck();
}
});
// 收到消息
ws.addEventListener("message", (event) => {
this.debug && console.debug(`websocket message:`, event);
// 没有开启心跳检测则直接触发事件,开启了心跳检测且返回数据不是指定的心跳响应也触发事件
if (!this.heartCheckEnable || this.heartCheckPong !== event.data) {
let message = event.data;
if (this.parseMessage) {
try {
message = JSON.parse(message);
} catch (error) {
this.debug &&
console.debug(
`WebSocketAuto parse message data fail`
);
}
}
this.dispatchEvent(
new MessageEvent("message", {
...event,
data: message,
})
);
}
if (this.heartCheckEnable) {
// 开始心跳检测
this.heartCheck();
}
});
// 连接关闭
ws.addEventListener("close", (event) => {
this.debug && console.debug(`websocket closed:`, event);
// 停止心跳检测
this.stopHeartCheck();
this.dispatchEvent(new CloseEvent("close", event));
// 如果不是主动被关闭,则尝试重连
if (!this.activeClose) {
this.reConnect();
}
});
// 连接出错
ws.addEventListener("error", (event) => {
this.debug && console.debug(`websocket error:`, event);
// 停止心跳检测
this.stopHeartCheck();
this.dispatchEvent(new ErrorEvent("error", event));
// 重连
this.reConnect();
});
}
/**
* @description 重连
* @ignore
*/
reConnect() {
// 如果设置了重连次数限制,超出限制则不再重连
if (
this.reConnectRepeatLimit &&
this.reConnectRepeatCount >= this.reConnectRepeatLimit
) {
return false;
}
// 延时重连,随着重连次数增加,重连间隔也增加,直到达到最大重连间隔
clearTimeout(this.reConnectTimmer);
let delay = this.baseReConnectTimeOut * (this.reConnectRepeatCount + 1);
if (delay > this.maxReConnectTimeOut) {
delay = this.maxReConnectTimeOut;
}
this.reConnectTimmer = setTimeout(() => {
this.debug && console.debug(`websocket reconnecting`);
this.reConnectRepeatCount++;
this.connect();
}, delay);
}
/**
* @description 发送消息
* @param {Object|String} data
*/
send(data) {
if (typeof data === "object") {
try {
data = JSON.stringify(data);
} catch (err) {
// do nothing
}
}
if (this.ws) {
if (this.ws.readyState === WebSocket.OPEN) {
this.debug && console.debug(`websocket send:`, data);
this.send(data);
} else {
console.warn(
`websocket send fail:ws readyState is ${this.ws.readyState},this message will be automatically sent when ws is ready.`
);
this.messages.push(data);
}
} else {
console.warn(
`websocket send fail:ws is not created,this message will be automatically sent when ws is ready.`
);
this.messages.push(data);
}
}
/**
* @description 手动关闭
*/
close(code = 1000, reason) {
this.activeClose = true;
return this.ws?.close(code, reason);
}
/**
* @description 心跳检测
* @ignore
*/
heartCheck() {
this.stopHeartCheck();
this.heartCheckTimer = setTimeout(() => {
// 发送心跳包
this.ws.send(this.heartCheckPing);
// 一定时间内无响应则关闭,触发重连
this.heartCheckCloseTimer = setTimeout(() => {
this.ws?.close();
}, this.heartCheckCloseTimeOut);
}, this.heartCheckTimeOut);
}
/**
* @description 停止心跳检测
* @ignore
*/
stopHeartCheck() {
clearTimeout(this.heartCheckTimer);
clearTimeout(this.heartCheckCloseTimer);
}
}