topfans/frontend/utils/socket/SocketManager.js
2026-05-28 12:00:19 +08:00

345 lines
11 KiB
JavaScript

import { getWebSocketBaseUrl } from '../api'
/**
* WebSocket 管理器
* 支持多个 WebSocket 连接,自动管理鉴权、心跳、重连
*/
class SocketManager {
constructor(options = {}) {
this.serviceName = options.serviceName || 'unknown'
this.baseUrl = 'ws://127.0.0.1:8080' // 占位符,等 connect() 时再获取真实地址
this.token = null
this.socket = null
this.heartbeatTimer = null
this.reconnectTimer = null
this.reconnectInterval = options.reconnectInterval || 3000
this.heartbeatInterval = options.heartbeatInterval || 30000
this.maxReconnectAttempts = options.maxReconnectAttempts || 5
this.reconnectAttempts = 0
// 状态
this.isConnected = false
this.isAuthed = false
this.isClosing = false // 标记是否主动关闭
// 事件处理器
this.eventHandlers = {
'connect': [],
'disconnect': [],
'auth_success': [],
'auth_fail': [],
'error': [],
'message': [] // 通用消息处理
}
// 子类可覆盖的消息类型处理
this.messageHandlers = {}
}
/**
* 连接到 WebSocket 服务器
*/
async connect(token, path) {
this.token = token
this.path = path
this.reconnectAttempts = 0
this.isClosing = false // 重置关闭状态,允许重连
// 异步获取真实 WebSocket 地址(等待环境检测完成)
this.baseUrl = await getWebSocketBaseUrl()
console.log(`[${this.serviceName}] WebSocket base URL: ${this.baseUrl}`)
this._doConnect()
}
_doConnect() {
// 如果已有连接且已连接,不重复连接
if (this.socket && this.isConnected) {
console.log(`[${this.serviceName}] Already connected (${this.isConnected}), skip reconnect`)
return
}
console.log(`[${this.serviceName}] _doConnect called, clearing old socket`)
// 清理旧连接
if (this.socket) {
this.socket = null
}
this.isConnected = false
const url = `${this.baseUrl}${this.path}?token=Bearer_${this.token}`
console.log(`[${this.serviceName}] Connecting to ${url}`)
// UniApp: connectSocket 是异步的,需要处理错误
try {
this.socket = uni.connectSocket({
url,
fail: (err) => {
console.error(`[${this.serviceName}] connectSocket fail:`, err)
this._emit('error', { code: 'CONNECT_FAILED', message: err.errMsg || '连接失败' })
}
})
if (!this.socket) {
console.error(`[${this.serviceName}] socket is null`)
return
}
console.log(`[${this.serviceName}] SocketTask created, checking methods:`, Object.keys(this.socket))
this._setupListeners()
} catch (err) {
console.error(`[${this.serviceName}] Exception during connect:`, err)
}
}
_setupListeners() {
// 检查 socket 是否有效
if (!this.socket) {
console.error(`[${this.serviceName}] socket is null in _setupListeners`)
return
}
// UniApp SocketTask API: onOpen/onClose/onError/onMessage 是设置回调的方法
// 也可能是事件名是 'open', 'close', 'error', 'message'
const socket = this.socket
const self = this
// 连接打开
if (typeof socket.onOpen === 'function') {
socket.onOpen(function() {
console.log(`[${self.serviceName}] WebSocket connected`)
self.isConnected = true
// 清除重连计时器
if (self.reconnectTimer) {
clearTimeout(self.reconnectTimer)
self.reconnectTimer = null
}
self.reconnectAttempts = 0
self._emit('connect')
})
} else if (typeof socket.onopen === 'function') {
// 标准 WebSocket 风格
socket.onopen(function() {
console.log(`[${self.serviceName}] WebSocket connected`)
self.isConnected = true
// 清除重连计时器
if (self.reconnectTimer) {
clearTimeout(self.reconnectTimer)
self.reconnectTimer = null
}
self.reconnectAttempts = 0
self._emit('connect')
})
} else {
console.warn(`[${self.serviceName}] Unknown socket API, socket keys:`, Object.keys(socket))
}
// 接收消息
if (typeof socket.onMessage === 'function') {
socket.onMessage(function(event) {
const data = JSON.parse(event.data)
self._handleMessage(data)
})
} else if (typeof socket.onmessage === 'function') {
socket.onmessage(function(event) {
const data = JSON.parse(event.data)
self._handleMessage(data)
})
}
// 连接关闭
if (typeof socket.onClose === 'function') {
socket.onClose(function() {
console.log(`[${self.serviceName}] WebSocket closed`)
self._cleanup()
self._emit('disconnect')
self._tryReconnect()
})
} else if (typeof socket.onclose === 'function') {
socket.onclose(function() {
console.log(`[${self.serviceName}] WebSocket closed`)
self._cleanup()
self._emit('disconnect')
self._tryReconnect()
})
}
// 连接错误
if (typeof socket.onError === 'function') {
socket.onError(function(err) {
console.error(`[${self.serviceName}] WebSocket error:`, err)
self._emit('error', err)
})
} else if (typeof socket.onerror === 'function') {
socket.onerror(function(err) {
console.error(`[${self.serviceName}] WebSocket error:`, err)
self._emit('error', err)
})
}
}
_handleMessage(data) {
// 触发通用消息事件
this._emit('message', data)
// 根据消息类型处理
const { type, action } = data
// 1. 鉴权响应(通用)
if (type === 'auth_response') {
if (data.success) {
this.isAuthed = true
this._emit('auth_success', data)
this._startHeartbeat()
} else {
this.isAuthed = false
this._emit('auth_fail', data)
this.close()
}
return
}
// 2. 心跳响应(通用)
if (type === 'pong') {
console.log(`[${this.serviceName}] Heartbeat received`)
return
}
// 3. 错误响应(通用)
if (type === 'error') {
this._emit('error', data)
return
}
// 4. 服务特定消息类型处理
const handler = this.messageHandlers[type] || this.messageHandlers[action]
if (handler) {
handler(data)
}
}
/**
* 发送消息
*/
send(data) {
if (!this.socket || !this.isConnected) {
console.warn(`[${this.serviceName}] Socket not connected`)
return false
}
// UniApp: socket.send({ data, success, fail })
if (typeof this.socket.send === 'function') {
this.socket.send({
data: JSON.stringify(data),
fail: (err) => {
console.error(`[${this.serviceName}] send fail:`, err)
}
})
} else {
console.warn(`[${this.serviceName}] socket.send is not a function`)
}
return true
}
/**
* 发送心跳
*/
_startHeartbeat() {
this._stopHeartbeat()
this.heartbeatTimer = setInterval(() => {
if (this.isConnected) {
this.send({ action: 'ping' })
}
}, this.heartbeatInterval)
}
_stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer)
this.heartbeatTimer = null
}
}
/**
* 重连机制
*/
_tryReconnect() {
if (this.isClosing) {
console.log(`[${this.serviceName}] Closing intentionally, skip reconnect`)
return
}
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.warn(`[${this.serviceName}] Max reconnect attempts reached`)
this._emit('error', { code: 'RECONNECT_FAILED', message: '重连次数已达上限' })
return
}
this.reconnectAttempts++
console.log(`[${this.serviceName}] Auto reconnecting... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
this.reconnectTimer = setTimeout(() => {
this._doConnect()
}, this.reconnectInterval)
}
_cleanup() {
this._stopHeartbeat()
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer)
this.reconnectTimer = null
}
this.isConnected = false
this.isAuthed = false
}
/**
* 关闭连接
*/
close() {
this.isClosing = true
this._cleanup()
if (this.socket) {
// UniApp: socket.close() 可能不存在,用 complete 代替
if (typeof this.socket.close === 'function') {
this.socket.close()
} else if (typeof this.socket.complete === 'function') {
this.socket.complete()
}
this.socket = null
}
}
// ===== 事件系统 =====
on(event, handler) {
if (this.eventHandlers[event]) {
this.eventHandlers[event].push(handler)
}
return () => this.off(event, handler) // 返回取消订阅函数
}
off(event, handler) {
if (this.eventHandlers[event]) {
this.eventHandlers[event] = this.eventHandlers[event].filter(h => h !== handler)
}
}
_emit(event, data) {
if (this.eventHandlers[event]) {
this.eventHandlers[event].forEach(handler => handler(data))
}
}
// ===== 订阅特定消息类型 =====
/**
* 注册特定消息类型的处理器
* @param {string} type 消息类型或 action
* @param {function} handler 处理函数
*/
registerHandler(type, handler) {
this.messageHandlers[type] = handler
}
}
/** 导出 */
export default SocketManager