import Taro from '@tarojs/taro'; import { getWsUrl } from '@/api/config'; import { get } from '@/api/request'; type MessageHandler = (data: unknown) => void; class WebSocketManager { private connected: boolean = false; private handlers: Map> = new Map(); private token: string = ''; private ticket: string = ''; private reconnectAttempts: number = 0; private maxReconnectAttempts: number = 5; private reconnectTimer: ReturnType | null = null; private isManualClose: boolean = false; async connect(token: string): Promise { this.token = token; this.isManualClose = false; await this.refreshTicketAndConnect(); } private doConnect(): void { if (!this.token || !this.ticket) { console.warn('[WS] 无 token,无法连接 WebSocket'); return; } // 先关闭旧连接 if (this.connected) { try { Taro.closeSocket(); } catch { // ignore } this.connected = false; } const url = getWsUrl(this.ticket); console.log('[WS] 正在连接:', url); // 注册全局回调(每次调用会替换上一次的回调) Taro.onSocketOpen(() => { console.log('[WS] 连接已建立'); this.connected = true; this.reconnectAttempts = 0; }); Taro.onSocketMessage((res) => { try { const data = JSON.parse(res.data as string); const type = (data.type || data.event || 'default') as string; const handlers = this.handlers.get(type); if (handlers) { handlers.forEach((handler) => handler(data)); } // 广播给通配符监听器 const wildcardHandlers = this.handlers.get('*'); if (wildcardHandlers) { wildcardHandlers.forEach((handler) => handler(data)); } } catch (e) { console.error('[WS] 消息解析失败:', e, res.data); } }); Taro.onSocketClose(() => { console.log('[WS] 连接已关闭'); this.connected = false; if (!this.isManualClose) { this.scheduleReconnect(); } }); Taro.onSocketError((err) => { console.error('[WS] 连接错误:', err); }); Taro.connectSocket({ url, fail: (err) => { console.error('[WS] 连接失败:', err); this.scheduleReconnect(); }, }); } private scheduleReconnect(): void { if (this.isManualClose) return; if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.warn('[WS] 达到最大重连次数,停止重连'); return; } this.reconnectAttempts++; const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000); console.log(`[WS] ${delay}ms 后第 ${this.reconnectAttempts} 次重连...`); if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); } this.reconnectTimer = setTimeout(() => { this.refreshTicketAndConnect(); }, delay); } private async refreshTicketAndConnect(): Promise { if (!this.token) return; try { const { ticket } = await get<{ ticket: string }>('/ws/ticket'); this.ticket = ticket; this.doConnect(); } catch (e) { console.error('[WS] 获取 ticket 失败:', e); this.scheduleReconnect(); } } on(type: string, handler: MessageHandler): () => void { if (!this.handlers.has(type)) { this.handlers.set(type, new Set()); } this.handlers.get(type)!.add(handler); return () => { this.handlers.get(type)?.delete(handler); }; } off(type: string, handler?: MessageHandler): void { if (handler) { this.handlers.get(type)?.delete(handler); } else { this.handlers.delete(type); } } send(data: unknown): void { if (this.connected) { Taro.sendSocketMessage({ data: typeof data === 'string' ? data : JSON.stringify(data), fail: (err) => { console.error('[WS] 发送失败:', err); }, }); } else { console.warn('[WS] WebSocket 未连接,无法发送消息'); } } close(): void { this.isManualClose = true; if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.connected) { Taro.closeSocket(); this.connected = false; } this.handlers.clear(); this.reconnectAttempts = 0; } isConnected(): boolean { return this.connected; } } export const wsManager = new WebSocketManager();