hldy_app_mini/common/websocket.js

320 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// common/websocket.js
// 带中文日志与消息体打印选项的 WsRequest替换你当前文件即可
class WsRequest {
constructor(url = '', options = {}) {
this.url = url || '';
this.options = Object.assign({
header: {},
protocols: [],
debug: true,
lang: 'zh', // 'zh' 或 'en',控制日志语言(默认中文)
showMessageBody: true, // 是否在日志中显示收到消息的完整 JSON默认 true
filterPing: true, // 是否过滤掉 ping/pong 日志(默认 true
heartbeatInterval: 30000,
heartbeatTimeout: 15000,
pingMessage: { type: 'ping' },
parseJSON: true,
maxReconnectAttempts: 10,
reconnectDelayBase: 1000,
autoConnect: false,
bindGlobal: true
}, options);
this.socketTask = null;
this.connected = false;
this._msgQueue = [];
this.reconnectAttempts = 0;
this._lastPongAt = Date.now();
this._hbTimer = null;
this._hbTimeoutTimer = null;
this._subscriptions = new Map();
if (this.options.bindGlobal) this._bindGlobalSocketEvents();
if (this.options.autoConnect) setTimeout(() => this.open(), 0);
}
// 内部多语言 Label
_label(key) {
const zh = {
open: '打开连接 ->',
alreadyConnected: '已连接,忽略 open',
connectNoTask: 'connectSocket 未返回 task使用全局回调',
onOpen: '连接已打开',
onMessage: '收到消息',
onClose: '连接已关闭',
onError: '连接错误',
globalOnOpen: '全局 onSocketOpen',
globalOnMessage: '全局 onSocketMessage',
hbTimeout: '心跳超时',
reconnectScheduled: '计划重连',
sendFailed: '发送失败',
closeError: '关闭错误'
};
const en = {
open: 'open ->',
alreadyConnected: 'already connected, ignore open',
connectNoTask: 'connectSocket returned no task (using global callbacks if available)',
onOpen: 'onOpen',
onMessage: 'onMessage',
onClose: 'onClose',
onError: 'onError',
globalOnOpen: 'global onSocketOpen',
globalOnMessage: 'global onSocketMessage',
hbTimeout: 'hb timeout',
reconnectScheduled: 'reconnect scheduled',
sendFailed: 'send failed',
closeError: 'close error'
};
return this.options.lang === 'zh' ? zh[key] || key : en[key] || key;
}
// 简单统一日志
log(...args) {
if (!this.options.debug) return;
// 如果是对象,格式化输出以便控制台可读
const out = args.map(a => {
if (typeof a === 'object') {
try { return JSON.stringify(a, null, 2); } catch (e) { return String(a); }
}
return String(a);
}).join(' ');
// console.log('[WsRequest]', out);
}
// ---------- 对外控制 ----------
open() {
if (!this.url) { this.log(this._label('open'), this._label('noUrl') || 'no url'); return; }
if (this.connected) { this.log(this._label('alreadyConnected')); return; }
this.log(this._label('open'), this.url);
try {
const task = uni.connectSocket({ url: this.url, header: this.options.header, protocols: this.options.protocols });
if (task && typeof task.onOpen === 'function') {
this._bindSocketTask(task);
} else {
this.log(this._label('connectNoTask'));
// 依赖全局回调_bindGlobalSocketEvents 已经绑定则会处理 onOpen/onMessage
}
} catch (e) {
console.error(e);
}
}
close(code = 1000, reason = 'client close') {
this._stopHeartbeat();
try {
if (this.socketTask && typeof this.socketTask.close === 'function') {
this.socketTask.close({ code, reason });
} else if (typeof uni.closeSocket === 'function') {
uni.closeSocket();
}
} catch (e) {
this.log(this._label('closeError'), e);
}
this.connected = false;
}
send(payload) {
const data = typeof payload === 'string' ? payload : JSON.stringify(payload);
if (this.socketTask && typeof this.socketTask.send === 'function' && this.connected) {
try { this.socketTask.send({ data }); return; } catch (e) { this.log(this._label('sendFailed'), e); this._msgQueue.push(data); }
} else {
try { uni.sendSocketMessage({ data }); return; } catch (e) { this._msgQueue.push(data); }
}
}
_flushQueue() {
if (!this.connected) return;
while (this._msgQueue.length) {
const d = this._msgQueue.shift();
try {
if (this.socketTask && typeof this.socketTask.send === 'function') {
this.socketTask.send({ data: d });
} else {
uni.sendSocketMessage({ data: d });
}
} catch (e) { this._msgQueue.unshift(d); break; }
}
}
_bindSocketTask(task) {
this.socketTask = task;
if (task.__ws_bound__) return;
task.__ws_bound__ = true;
task.onOpen(res => {
this.log(this._label('onOpen'), res);
this.connected = true;
this.reconnectAttempts = 0;
this._lastPongAt = Date.now();
this._startHeartbeat();
this._flushQueue();
this.options.onOpen && this.options.onOpen(res);
});
task.onMessage(msg => {
this._handleIncoming(msg);
});
task.onClose(res => {
this.log(this._label('onClose'), res);
this.connected = false;
this._stopHeartbeat();
this.options.onClose && this.options.onClose(res);
this._tryReconnect();
});
task.onError(err => {
this.log(this._label('onError'), err);
this.connected = false;
this._stopHeartbeat();
this.options.onError && this.options.onError(err);
this._tryReconnect();
});
}
// 处理收到的消息task 或 全局都使用)
_handleIncoming(msg) {
this._lastPongAt = Date.now();
let data = msg && msg.data;
if (this.options.parseJSON) {
try { data = JSON.parse(msg.data); } catch (e) { /* keep raw */ }
}
const type = (data && data.type) || '__default__';
// 过滤 ping/pong (若你想看 ping/pong把 filterPing 设为 false
const isPing = data && (data.type === 'ping' || data.type === 'pong' || (typeof data === 'string' && (data === 'ping' || data === 'pong')));
if (isPing && this.options.filterPing) {
// 仍更新 lastPong但不打印消息体除非 showMessageBody 强制 true
if (this.options.debug && this.options.showMessageBody === false) {
this.log(this._label('onMessage'), type);
}
} else {
// 打印消息类型与(可选)格式化消息体
if (this.options.debug) {
if (this.options.showMessageBody) {
let bodyStr;
try { bodyStr = JSON.stringify(data, null, 2); } catch (e) { bodyStr = String(data); }
this.log(this.options.lang === 'zh' ? `${this._label('onMessage')} (${type}):` : `${this._label('onMessage')} (${type}):`, '\n' + bodyStr);
} else {
this.log(this._label('onMessage'), type);
}
}
}
// 外部回调与订阅异步派发
if (this.options.onMessage) setTimeout(() => this.options.onMessage(data, msg), 0);
setTimeout(() => this._dispatch(type, data, msg), 0);
}
_bindGlobalSocketEvents() {
// 解绑默认全局再绑定,避免重复
try { uni.offSocketOpen && uni.offSocketOpen(); } catch (e) {}
try { uni.offSocketMessage && uni.offSocketMessage(); } catch (e) {}
try { uni.offSocketClose && uni.offSocketClose(); } catch (e) {}
try { uni.offSocketError && uni.offSocketError(); } catch (e) {}
try {
uni.onSocketOpen(res => {
this.log(this._label('globalOnOpen'), res);
this.connected = true;
this.reconnectAttempts = 0;
this._lastPongAt = Date.now();
this._startHeartbeat();
this._flushQueue();
this.options.onOpen && this.options.onOpen(res);
});
} catch (e) {}
try {
uni.onSocketMessage(msg => {
// 全局收到消息也走统一处理
if (this.options.debug) this.log(this._label('globalOnMessage'));
this._handleIncoming(msg);
});
} catch (e) {}
try {
uni.onSocketClose(res => {
this.log(this._label('globalOnClose'), res);
this.connected = false;
this._stopHeartbeat();
this.options.onClose && this.options.onClose(res);
this._tryReconnect();
});
} catch (e) {}
try {
uni.onSocketError(err => {
this.log(this._label('globalOnError'), err);
this.connected = false;
this._stopHeartbeat();
this.options.onError && this.options.onError(err);
this._tryReconnect();
});
} catch (e) {}
}
_startHeartbeat() {
this._stopHeartbeat();
if (!this.options.heartbeatInterval) return;
const pingData = typeof this.options.pingMessage === 'string' ? this.options.pingMessage : JSON.stringify(this.options.pingMessage);
this._hbTimer = setInterval(() => {
if (!this.connected) return;
try { this.send(pingData); } catch (e) { this.log('hb send err', e); }
clearTimeout(this._hbTimeoutTimer);
this._hbTimeoutTimer = setTimeout(() => {
const since = Date.now() - this._lastPongAt;
if (since > this.options.heartbeatTimeout) {
this.log(this._label('hbTimeout'), since);
try { this.close(); } catch (e) { this.log('hb close fail', e); }
}
}, this.options.heartbeatTimeout);
}, this.options.heartbeatInterval);
}
_stopHeartbeat() {
if (this._hbTimer) { clearInterval(this._hbTimer); this._hbTimer = null; }
if (this._hbTimeoutTimer) { clearTimeout(this._hbTimeoutTimer); this._hbTimeoutTimer = null; }
}
_tryReconnect() {
if (this.reconnectAttempts >= this.options.maxReconnectAttempts) { this.log(this._label('reconnectScheduled'), 'exhausted'); return; }
this.reconnectAttempts++;
const delay = Math.min(this.options.reconnectDelayBase * Math.pow(1.5, this.reconnectAttempts - 1), 30000);
this.log(this._label('reconnectScheduled'), delay + 'ms', 'attempt', this.reconnectAttempts);
setTimeout(() => {
this.socketTask = null;
this.connectIfForeground();
}, delay);
}
connectIfForeground() {
// 如果你在项目中有前台检测,这里可改为判断前台才 open()
this.open();
}
subscribe(type, handler) {
if (!this._subscriptions) this._subscriptions = new Map();
if (!this._subscriptions.has(type)) this._subscriptions.set(type, new Set());
this._subscriptions.get(type).add(handler);
return () => this.unsubscribe(type, handler);
}
unsubscribe(type, handler) {
if (!this._subscriptions) return;
const s = this._subscriptions.get(type); if (!s) return; s.delete(handler);
if (s.size === 0) this._subscriptions.delete(type);
}
_dispatch(type, data, raw) {
if (!this._subscriptions) return;
const handlers = this._subscriptions.get(type);
if (handlers) for (const h of Array.from(handlers)) try { h(data, raw); } catch (e) { this.log('handler err', e); }
const def = this._subscriptions.get('__default__');
if (def) for (const h of Array.from(def)) try { h(data, raw); } catch (e) { this.log('default handler err', e); }
}
}
export default WsRequest;