170 lines
4.4 KiB
TypeScript
170 lines
4.4 KiB
TypeScript
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<string, Set<MessageHandler>> = new Map();
|
|
private token: string = '';
|
|
private ticket: string = '';
|
|
private reconnectAttempts: number = 0;
|
|
private maxReconnectAttempts: number = 5;
|
|
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
private isManualClose: boolean = false;
|
|
|
|
async connect(token: string): Promise<void> {
|
|
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<void> {
|
|
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();
|