mi-assessment/uniapp/utils/signalr.js
2026-02-09 14:45:06 +08:00

473 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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