473 lines
11 KiB
JavaScript
473 lines
11 KiB
JavaScript
/**
|
||
* SignalR 客户端封装
|
||
* 用于小程序实时通信
|
||
*/
|
||
|
||
import { getToken } from './storage'
|
||
import config from '../config/index'
|
||
|
||
// SignalR 协议分隔符
|
||
const RECORD_SEPARATOR = '\x1e'
|
||
|
||
// 消息类型
|
||
const MessageTypes = {
|
||
Invocation: 1,
|
||
StreamItem: 2,
|
||
Completion: 3,
|
||
StreamInvocation: 4,
|
||
CancelInvocation: 5,
|
||
Ping: 6,
|
||
Close: 7
|
||
}
|
||
|
||
class SignalRClient {
|
||
constructor() {
|
||
this.socketTask = null
|
||
this.isConnected = false
|
||
this.isConnecting = false
|
||
this.reconnectAttempts = 0
|
||
this.maxReconnectAttempts = 5
|
||
this.reconnectDelay = 1000
|
||
this.heartbeatInterval = null
|
||
this.messageHandlers = new Map()
|
||
this.invocationId = 0
|
||
this.pendingCalls = new Map()
|
||
}
|
||
|
||
/**
|
||
* 连接到 SignalR Hub
|
||
*/
|
||
async connect() {
|
||
if (this.isConnected || this.isConnecting) {
|
||
console.log('[SignalR] 已连接或正在连接中')
|
||
return Promise.resolve()
|
||
}
|
||
|
||
const token = getToken()
|
||
if (!token) {
|
||
console.error('[SignalR] 未登录,无法连接')
|
||
return Promise.reject(new Error('未登录'))
|
||
}
|
||
|
||
this.isConnecting = true
|
||
|
||
return new Promise((resolve, reject) => {
|
||
try {
|
||
// 使用配置中的 SignalR URL
|
||
const wsUrl = config.SIGNALR_URL
|
||
|
||
console.log('[SignalR] 正在连接:', wsUrl)
|
||
|
||
this.socketTask = uni.connectSocket({
|
||
url: `${wsUrl}?access_token=${encodeURIComponent(token)}`,
|
||
header: {
|
||
'Authorization': `Bearer ${token}`
|
||
},
|
||
success: () => {
|
||
console.log('[SignalR] WebSocket 创建成功')
|
||
},
|
||
fail: (err) => {
|
||
console.error('[SignalR] WebSocket 创建失败:', err)
|
||
this.isConnecting = false
|
||
reject(err)
|
||
}
|
||
})
|
||
|
||
// 监听连接打开
|
||
this.socketTask.onOpen(() => {
|
||
console.log('[SignalR] WebSocket 连接已打开')
|
||
this.sendHandshake()
|
||
})
|
||
|
||
// 监听消息
|
||
this.socketTask.onMessage((res) => {
|
||
this.handleMessage(res.data, resolve)
|
||
})
|
||
|
||
// 监听连接关闭
|
||
this.socketTask.onClose((res) => {
|
||
console.log('[SignalR] WebSocket 连接已关闭:', res)
|
||
this.handleClose()
|
||
})
|
||
|
||
// 监听错误
|
||
this.socketTask.onError((err) => {
|
||
console.error('[SignalR] WebSocket 错误:', err)
|
||
this.isConnecting = false
|
||
reject(err)
|
||
})
|
||
|
||
} catch (error) {
|
||
console.error('[SignalR] 连接异常:', error)
|
||
this.isConnecting = false
|
||
reject(error)
|
||
}
|
||
})
|
||
}
|
||
|
||
|
||
/**
|
||
* 发送握手消息
|
||
*/
|
||
sendHandshake() {
|
||
const handshake = {
|
||
protocol: 'json',
|
||
version: 1
|
||
}
|
||
this.sendRaw(JSON.stringify(handshake) + RECORD_SEPARATOR)
|
||
}
|
||
|
||
/**
|
||
* 处理接收到的消息
|
||
*/
|
||
handleMessage(data, connectResolve) {
|
||
try {
|
||
// SignalR 消息以 \x1e 分隔
|
||
const messages = data.split(RECORD_SEPARATOR).filter(m => m.trim())
|
||
|
||
messages.forEach(msg => {
|
||
try {
|
||
const message = JSON.parse(msg)
|
||
|
||
// 握手响应(空对象表示成功)
|
||
if (Object.keys(message).length === 0 || message.type === undefined) {
|
||
console.log('[SignalR] 握手成功')
|
||
this.isConnected = true
|
||
this.isConnecting = false
|
||
this.reconnectAttempts = 0
|
||
this.startHeartbeat()
|
||
if (connectResolve) {
|
||
connectResolve()
|
||
}
|
||
return
|
||
}
|
||
|
||
switch (message.type) {
|
||
case MessageTypes.Invocation:
|
||
this.handleInvocation(message)
|
||
break
|
||
case MessageTypes.Completion:
|
||
this.handleCompletion(message)
|
||
break
|
||
case MessageTypes.Ping:
|
||
// 收到 Ping,发送 Pong
|
||
this.sendPing()
|
||
break
|
||
case MessageTypes.Close:
|
||
console.log('[SignalR] 服务端关闭连接:', message.error)
|
||
this.disconnect()
|
||
break
|
||
default:
|
||
console.log('[SignalR] 未知消息类型:', message.type)
|
||
}
|
||
} catch (parseError) {
|
||
console.error('[SignalR] 解析消息失败:', parseError, msg)
|
||
}
|
||
})
|
||
} catch (error) {
|
||
console.error('[SignalR] 处理消息失败:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理服务端调用
|
||
*/
|
||
handleInvocation(message) {
|
||
const { target, arguments: args } = message
|
||
console.log('[SignalR] 收到服务端调用:', target, args)
|
||
|
||
// 触发对应的事件处理器
|
||
const handlers = this.messageHandlers.get(target)
|
||
if (handlers && handlers.length > 0) {
|
||
handlers.forEach(handler => {
|
||
try {
|
||
handler(...(args || []))
|
||
} catch (err) {
|
||
console.error('[SignalR] 处理器执行错误:', err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理方法调用完成
|
||
*/
|
||
handleCompletion(message) {
|
||
const { invocationId, result, error } = message
|
||
const pending = this.pendingCalls.get(invocationId)
|
||
|
||
if (pending) {
|
||
this.pendingCalls.delete(invocationId)
|
||
if (error) {
|
||
pending.reject(new Error(error))
|
||
} else {
|
||
pending.resolve(result)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理连接关闭
|
||
*/
|
||
handleClose() {
|
||
this.isConnected = false
|
||
this.isConnecting = false
|
||
this.stopHeartbeat()
|
||
|
||
// 尝试重连
|
||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||
this.scheduleReconnect()
|
||
} else {
|
||
console.error('[SignalR] 重连次数已达上限')
|
||
// 触发断开连接事件
|
||
const handlers = this.messageHandlers.get('Disconnected')
|
||
if (handlers) {
|
||
handlers.forEach(h => h())
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 安排重连
|
||
*/
|
||
scheduleReconnect() {
|
||
this.reconnectAttempts++
|
||
const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), 30000)
|
||
|
||
console.log(`[SignalR] ${delay}ms 后尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
|
||
|
||
setTimeout(() => {
|
||
if (!this.isConnected && !this.isConnecting) {
|
||
this.connect().catch(err => {
|
||
console.error('[SignalR] 重连失败:', err)
|
||
})
|
||
}
|
||
}, delay)
|
||
}
|
||
|
||
/**
|
||
* 开始心跳
|
||
*/
|
||
startHeartbeat() {
|
||
this.stopHeartbeat()
|
||
this.heartbeatInterval = setInterval(() => {
|
||
if (this.isConnected) {
|
||
this.sendPing()
|
||
}
|
||
}, 15000) // 每15秒发送一次心跳
|
||
}
|
||
|
||
/**
|
||
* 停止心跳
|
||
*/
|
||
stopHeartbeat() {
|
||
if (this.heartbeatInterval) {
|
||
clearInterval(this.heartbeatInterval)
|
||
this.heartbeatInterval = null
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发送 Ping
|
||
*/
|
||
sendPing() {
|
||
const ping = { type: MessageTypes.Ping }
|
||
this.sendRaw(JSON.stringify(ping) + RECORD_SEPARATOR)
|
||
}
|
||
|
||
/**
|
||
* 发送原始数据
|
||
*/
|
||
sendRaw(data) {
|
||
if (!this.socketTask) {
|
||
console.error('[SignalR] WebSocket 未连接')
|
||
return false
|
||
}
|
||
|
||
this.socketTask.send({
|
||
data,
|
||
success: () => {
|
||
// console.log('[SignalR] 发送成功')
|
||
},
|
||
fail: (err) => {
|
||
console.error('[SignalR] 发送失败:', err)
|
||
}
|
||
})
|
||
return true
|
||
}
|
||
|
||
|
||
/**
|
||
* 调用服务端方法
|
||
* @param {string} method 方法名
|
||
* @param {...any} args 参数
|
||
* @returns {Promise}
|
||
*/
|
||
invoke(method, ...args) {
|
||
return new Promise((resolve, reject) => {
|
||
if (!this.isConnected) {
|
||
reject(new Error('SignalR 未连接'))
|
||
return
|
||
}
|
||
|
||
const invocationId = String(++this.invocationId)
|
||
|
||
const message = {
|
||
type: MessageTypes.Invocation,
|
||
invocationId,
|
||
target: method,
|
||
arguments: args
|
||
}
|
||
|
||
this.pendingCalls.set(invocationId, { resolve, reject })
|
||
|
||
// 设置超时
|
||
setTimeout(() => {
|
||
if (this.pendingCalls.has(invocationId)) {
|
||
this.pendingCalls.delete(invocationId)
|
||
reject(new Error('调用超时'))
|
||
}
|
||
}, 30000)
|
||
|
||
this.sendRaw(JSON.stringify(message) + RECORD_SEPARATOR)
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 发送消息(不等待响应)
|
||
* @param {string} method 方法名
|
||
* @param {...any} args 参数
|
||
*/
|
||
send(method, ...args) {
|
||
if (!this.isConnected) {
|
||
console.error('[SignalR] 未连接,无法发送')
|
||
return false
|
||
}
|
||
|
||
const message = {
|
||
type: MessageTypes.Invocation,
|
||
target: method,
|
||
arguments: args
|
||
}
|
||
|
||
return this.sendRaw(JSON.stringify(message) + RECORD_SEPARATOR)
|
||
}
|
||
|
||
/**
|
||
* 监听服务端事件
|
||
* @param {string} event 事件名
|
||
* @param {Function} callback 回调函数
|
||
*/
|
||
on(event, callback) {
|
||
if (!this.messageHandlers.has(event)) {
|
||
this.messageHandlers.set(event, [])
|
||
}
|
||
this.messageHandlers.get(event).push(callback)
|
||
console.log('[SignalR] 注册事件监听:', event)
|
||
}
|
||
|
||
/**
|
||
* 移除事件监听
|
||
* @param {string} event 事件名
|
||
* @param {Function} callback 回调函数(可选,不传则移除所有)
|
||
*/
|
||
off(event, callback) {
|
||
if (!callback) {
|
||
this.messageHandlers.delete(event)
|
||
} else {
|
||
const handlers = this.messageHandlers.get(event)
|
||
if (handlers) {
|
||
const index = handlers.indexOf(callback)
|
||
if (index > -1) {
|
||
handlers.splice(index, 1)
|
||
}
|
||
if (handlers.length === 0) {
|
||
this.messageHandlers.delete(event)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 断开连接
|
||
*/
|
||
disconnect() {
|
||
console.log('[SignalR] 主动断开连接')
|
||
this.stopHeartbeat()
|
||
this.isConnected = false
|
||
this.isConnecting = false
|
||
this.reconnectAttempts = this.maxReconnectAttempts // 阻止自动重连
|
||
|
||
if (this.socketTask) {
|
||
this.socketTask.close({
|
||
success: () => {
|
||
console.log('[SignalR] 连接已关闭')
|
||
}
|
||
})
|
||
this.socketTask = null
|
||
}
|
||
|
||
// 清理待处理的调用
|
||
this.pendingCalls.forEach((pending) => {
|
||
pending.reject(new Error('连接已断开'))
|
||
})
|
||
this.pendingCalls.clear()
|
||
}
|
||
|
||
/**
|
||
* 重置并重新连接
|
||
*/
|
||
async reconnect() {
|
||
this.disconnect()
|
||
this.reconnectAttempts = 0
|
||
await new Promise(resolve => setTimeout(resolve, 100))
|
||
return this.connect()
|
||
}
|
||
|
||
/**
|
||
* 加入会话组
|
||
* @param {number} sessionId 会话ID
|
||
*/
|
||
async joinSession(sessionId) {
|
||
if (!this.isConnected) {
|
||
console.warn('[SignalR] 未连接,无法加入会话')
|
||
return
|
||
}
|
||
try {
|
||
await this.invoke('JoinSession', sessionId)
|
||
console.log('[SignalR] 已加入会话:', sessionId)
|
||
} catch (err) {
|
||
console.error('[SignalR] 加入会话失败:', err)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 离开会话组
|
||
* @param {number} sessionId 会话ID
|
||
*/
|
||
async leaveSession(sessionId) {
|
||
if (!this.isConnected) {
|
||
return
|
||
}
|
||
try {
|
||
await this.invoke('LeaveSession', sessionId)
|
||
console.log('[SignalR] 已离开会话:', sessionId)
|
||
} catch (err) {
|
||
console.error('[SignalR] 离开会话失败:', err)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取连接状态
|
||
*/
|
||
get connected() {
|
||
return this.isConnected
|
||
}
|
||
}
|
||
|
||
// 创建单例实例
|
||
const signalR = new SignalRClient()
|
||
|
||
export { signalR, SignalRClient }
|
||
export default signalR
|