hldy_app_mini/common/websocket.js

382 lines
13 KiB
JavaScript
Raw Normal View History

2025-12-30 08:40:09 +08:00
// common/websocket.js
// 带中文日志与消息体打印选项的 WsRequest替换你当前文件即可
class WsRequest {
2026-01-19 17:35:31 +08:00
// 静态:防止重复全局绑定
static _globalBound = false;
2025-12-30 08:40:09 +08:00
constructor(url = '', options = {}) {
this.url = url || '';
this.options = Object.assign({
header: {},
protocols: [],
debug: true,
2026-01-19 17:35:31 +08:00
lang: 'zh', // 'zh' 或 'en'
showMessageBody: true,
filterPing: true,
2025-12-30 08:40:09 +08:00
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();
2026-01-19 17:35:31 +08:00
// 是否允许自动重连(主动 close 时设为 false
this._shouldReconnect = true;
this._manualClose = false;
// 如果要求全局绑定且还没绑定过,则绑定一次(静态控制)
if (this.options.bindGlobal && !WsRequest._globalBound) {
this._bindGlobalSocketEvents();
WsRequest._globalBound = true;
} else if (this.options.bindGlobal && WsRequest._globalBound) {
// 已由其他实例绑定,跳过
}
2025-12-30 08:40:09 +08:00
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;
}
2026-01-19 17:35:31 +08:00
// 简单统一日志(恢复 console 输出,便于调试)
2025-12-30 08:40:09 +08:00
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(' ');
2026-01-14 16:00:57 +08:00
// console.log('[WsRequest]', out);
2025-12-30 08:40:09 +08:00
}
// ---------- 对外控制 ----------
open() {
if (!this.url) { this.log(this._label('open'), this._label('noUrl') || 'no url'); return; }
if (this.connected) { this.log(this._label('alreadyConnected')); return; }
2026-01-19 17:35:31 +08:00
2025-12-30 08:40:09 +08:00
this.log(this._label('open'), this.url);
2026-01-19 17:35:31 +08:00
// 主动 open允许后续自动重连如果被之前的手动关闭禁止过open 表示想要恢复)
this._shouldReconnect = true;
this._manualClose = false;
2025-12-30 08:40:09 +08:00
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'));
2026-01-19 17:35:31 +08:00
// 依赖全局回调,已在构造时绑定
2025-12-30 08:40:09 +08:00
}
} catch (e) {
console.error(e);
}
}
2026-01-19 17:35:31 +08:00
/**
* close(code, reason, manual)
* manual: 是否为手动/意图性关闭默认 true
* - manual === true 时将禁止自动重连用于用户主动关闭
* - manual === false 时视为异常或内部触发的关闭允许重连
*/
close(code = 1000, reason = 'client close', manual = true) {
// 标记手动关闭以禁止后续自动重连
if (manual) {
this._shouldReconnect = false;
this._manualClose = true;
} else {
// 非手动关闭(例如心跳超时)则保持 _shouldReconnect 原有值(通常为 true
this._manualClose = false;
}
2025-12-30 08:40:09 +08:00
this._stopHeartbeat();
try {
if (this.socketTask && typeof this.socketTask.close === 'function') {
2026-01-19 17:35:31 +08:00
try {
this.socketTask.close({ code, reason });
} catch (e) {
// 某些平台 socketTask.close 可能会抛错
this.log(this._label('closeError'), e);
// 尝试全局 close
if (typeof uni.closeSocket === 'function') uni.closeSocket();
}
2025-12-30 08:40:09 +08:00
} else if (typeof uni.closeSocket === 'function') {
uni.closeSocket();
}
} catch (e) {
this.log(this._label('closeError'), e);
}
2026-01-19 17:35:31 +08:00
2025-12-30 08:40:09 +08:00
this.connected = false;
2026-01-19 17:35:31 +08:00
// 清理 socketTask 引用,释放资源,防止重复使用导致 FD 泄漏
this.socketTask = null;
2025-12-30 08:40:09 +08:00
}
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);
2026-01-19 17:35:31 +08:00
// 打开时允许后续重连(如果连接成功)
this._shouldReconnect = true;
this._manualClose = false;
2025-12-30 08:40:09 +08:00
});
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);
2026-01-19 17:35:31 +08:00
// 仅当允许重连时才触发重连逻辑
2025-12-30 08:40:09 +08:00
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__';
2026-01-19 17:35:31 +08:00
// 过滤 ping/pong
2025-12-30 08:40:09 +08:00
const isPing = data && (data.type === 'ping' || data.type === 'pong' || (typeof data === 'string' && (data === 'ping' || data === 'pong')));
if (isPing && this.options.filterPing) {
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() {
2026-01-19 17:35:31 +08:00
// 解绑默认全局再绑定,避免重复(但是本函数只会被调用一次,受静态保护)
2025-12-30 08:40:09 +08:00
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);
2026-01-19 17:35:31 +08:00
// 同上,打开时允许重连
this._shouldReconnect = true;
this._manualClose = false;
2025-12-30 08:40:09 +08:00
});
} 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);
2026-01-19 17:35:31 +08:00
// 由心跳触发的关闭视为“异常/内部”关闭 — 传 manual = false 以允许重连
try { this.close(1000, 'hb timeout', false); } catch (e) { this.log('hb close fail', e); }
2025-12-30 08:40:09 +08:00
}
}, 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() {
2026-01-19 17:35:31 +08:00
// 先检查是否允许自动重连(主动关闭时应被禁用)
if (!this._shouldReconnect) {
this.log(this._label('reconnectScheduled'), 'reconnect disabled (manual close)');
return;
}
2025-12-30 08:40:09 +08:00
if (this.reconnectAttempts >= this.options.maxReconnectAttempts) { this.log(this._label('reconnectScheduled'), 'exhausted'); return; }
this.reconnectAttempts++;
2026-01-19 17:35:31 +08:00
// 指数退避 + jitter避免同时大量重连
const base = this.options.reconnectDelayBase || 1000;
const delay = Math.min(base * Math.pow(1.5, this.reconnectAttempts - 1), 30000);
const jitter = Math.floor(Math.random() * 401) - 200; // -200 .. +200 ms
const finalDelay = Math.max(0, Math.floor(delay) + jitter);
this.log(this._label('reconnectScheduled'), finalDelay + 'ms', 'attempt', this.reconnectAttempts);
2025-12-30 08:40:09 +08:00
setTimeout(() => {
this.socketTask = null;
this.connectIfForeground();
2026-01-19 17:35:31 +08:00
}, finalDelay);
2025-12-30 08:40:09 +08:00
}
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;