first commit
This commit is contained in:
366
utils/socket.js
Normal file
366
utils/socket.js
Normal file
@@ -0,0 +1,366 @@
|
||||
const WS_APP_URL = 'ws://cloud_test.yuxindazhineng.com'
|
||||
// (工具类)连接、发送、接收、重连...
|
||||
class WebSocketManager {
|
||||
constructor() {
|
||||
this.ws = null
|
||||
this.url = `${WS_APP_URL}/cloud_api/phone/app_ws`
|
||||
this.options = {
|
||||
token: '',
|
||||
conversationId: '',
|
||||
onMessage: null,
|
||||
onError: null,
|
||||
onReconnect: null,
|
||||
onOpen: null,
|
||||
onClose: null
|
||||
}
|
||||
this.reconnectAttempts = 0
|
||||
this.maxReconnectAttempts = 5
|
||||
this.reconnectDelay = 3000
|
||||
this.reconnectTimer = null
|
||||
this.heartbeatTimer = null
|
||||
this.isManualClose = false
|
||||
}
|
||||
/**
|
||||
* 初始化连接
|
||||
* @param {string} url WebSocket URL
|
||||
* @param {Object} options 配置选项
|
||||
*/
|
||||
connect(options = {}) {
|
||||
this.options = {
|
||||
...this.options,
|
||||
...options
|
||||
}
|
||||
this.isManualClose = false
|
||||
// 关闭现有连接
|
||||
if (this.ws) {
|
||||
this.close()
|
||||
}
|
||||
try {
|
||||
// #ifdef APP-PLUS || MP-WEIXIN
|
||||
this.ws = uni.connectSocket({
|
||||
url: this.url,
|
||||
success: () => {
|
||||
console.log('WebSocket 连接请求已发送');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('WebSocket 连接失败:', err)
|
||||
this.handleError(err)
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
if (!this.ws) {
|
||||
this.ws = new WebSocket(this.url)
|
||||
}
|
||||
// #endif
|
||||
|
||||
this.initEventHandlers()
|
||||
} catch (err) {
|
||||
this.handleError(err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化事件处理
|
||||
*/
|
||||
initEventHandlers() {
|
||||
if (!this.ws) return
|
||||
|
||||
// #ifdef APP-PLUS || MP-WEIXIN
|
||||
// 打开连接
|
||||
this.ws.onOpen((event) => {
|
||||
console.log('WebSocket 已连接')
|
||||
// 修复:先判断重连状态,再重置计数
|
||||
if (this.reconnectAttempts > 0) {
|
||||
this.options.onReconnect?.()
|
||||
}
|
||||
this.options.onOpen?.(event)
|
||||
this.reconnectAttempts = 0
|
||||
// this.startHeartbeat()
|
||||
})
|
||||
this.ws.onMessage((event) => {
|
||||
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
console.log('收到了返回消息:', data);
|
||||
// uni.setStorageSync('currentSessionId', data.task_call_id)
|
||||
this.handleMessage(data)
|
||||
} catch (err) {
|
||||
console.error('消息解析失败:', err)
|
||||
uni.setStorageSync('currentSessionId', event.data.task_call_id)
|
||||
}
|
||||
})
|
||||
this.ws.onError((err) => {
|
||||
console.error('WebSocket 错误:', err)
|
||||
this.handleError(err)
|
||||
})
|
||||
this.ws.onClose((event) => {
|
||||
console.log('WebSocket 已关闭:', event.code, event.reason)
|
||||
this.options.onClose?.(event)
|
||||
this.stopHeartbeat()
|
||||
if (!this.isManualClose && this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.reconnect()
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
// H5 环境使用原生 WebSocket API
|
||||
if (typeof WebSocket !== 'undefined') {
|
||||
this.ws.onopen = (event) => {
|
||||
console.log('WebSocket 已连接 (H5)')
|
||||
if (this.reconnectAttempts > 0) {
|
||||
this.options.onReconnect?.()
|
||||
}
|
||||
this.options.onOpen?.(event)
|
||||
this.reconnectAttempts = 0
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
console.log('收到了返回消息 (H5):', data);
|
||||
this.handleMessage(data)
|
||||
} catch (err) {
|
||||
console.error('消息解析失败 (H5):', err)
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onerror = (err) => {
|
||||
console.error('WebSocket 错误 (H5):', err)
|
||||
this.handleError(err)
|
||||
}
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
console.log('WebSocket 已关闭 (H5):', event.code, event.reason)
|
||||
this.options.onClose?.(event)
|
||||
this.stopHeartbeat()
|
||||
if (!this.isManualClose && this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.reconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理服务端推送的消息
|
||||
* @param {Object} msg - 服务端返回的原始消息对象
|
||||
*/
|
||||
handleMessage(msg) {
|
||||
this.options.onMessage?.(msg)
|
||||
const type = msg.type
|
||||
// const data = msg.data || {}
|
||||
// 连接确认:服务端认证成功响应
|
||||
if (type === 'auth_ok') {
|
||||
// 发送消息携带的上一次返回的验证id
|
||||
// const taskCallId = data.task_call_id
|
||||
// uni.setStorageSync('taskCallId', taskCallId)
|
||||
console.log('认证成功,socket连接成功');
|
||||
return
|
||||
}
|
||||
|
||||
// 断开连接指令:服务端主动要求断开
|
||||
if (type === 'auth_fail') {
|
||||
if(msg?.reason === "pc offline")
|
||||
console.log("pc端不在线,不可聊天");
|
||||
// console.log('服务端要求断开连接')
|
||||
this.isManualClose = true
|
||||
this.close()
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一发送消息方法
|
||||
* 兼容 APP / 微信小程序 环境
|
||||
* @param {Object} data - 要发送的消息对象
|
||||
* @returns {Promise} 发送结果
|
||||
*/
|
||||
send(data) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 未连接直接拒绝
|
||||
if (!this.isConnected()) {
|
||||
reject(new Error('WebSocket 未连接'))
|
||||
return
|
||||
}
|
||||
const msg = JSON.stringify(data)
|
||||
console.log("发送的消息是:", msg);
|
||||
|
||||
// #ifdef APP-PLUS || MP-WEIXIN
|
||||
// APP / 小程序环境:uni-app 封装 API
|
||||
this.ws.send({
|
||||
data: msg,
|
||||
success: () => resolve(),
|
||||
fail: (err) => {
|
||||
console.error('发送消息失败:', err)
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
// H5 环境:原生 WebSocket API
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(msg)
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error('WebSocket 未打开'))
|
||||
}
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动重连机制
|
||||
* 指数退避重连,避免频繁重连
|
||||
*/
|
||||
reconnect() {
|
||||
// 已有重连定时器则不重复触发
|
||||
if (this.reconnectTimer) return
|
||||
this.reconnectAttempts++
|
||||
console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`)
|
||||
// 延迟执行重连,延迟时间随次数递增
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectTimer = null
|
||||
// 非手动关闭才重连
|
||||
if (!this.isManualClose) {
|
||||
this.connect(this.options)
|
||||
}
|
||||
}, this.reconnectDelay * this.reconnectAttempts)
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动心跳定时器
|
||||
* 每30秒发送一次 ping 保持连接
|
||||
*/
|
||||
startHeartbeat() {
|
||||
this.stopHeartbeat()
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
if (this.isConnected()) {
|
||||
this.send({
|
||||
ws_event: 'ping'
|
||||
}).catch(() => {
|
||||
console.log('心跳发送失败,准备重连')
|
||||
// 避免在已经重连过程中重复触发
|
||||
if (!this.reconnectTimer && !this.isManualClose) {
|
||||
this.reconnect()
|
||||
}
|
||||
})
|
||||
} else if (!this.isManualClose && this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
// 连接已断开但未触发 close 事件时主动重连
|
||||
this.reconnect()
|
||||
}
|
||||
|
||||
}, 30000)
|
||||
}
|
||||
/**
|
||||
* 停止心跳定时器
|
||||
*/
|
||||
stopHeartbeat() {
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer)
|
||||
this.heartbeatTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一错误处理
|
||||
* @param {Error} error - 错误对象
|
||||
*/
|
||||
handleError(error) {
|
||||
console.error('WebSocket 错误处理:', JSON.stringify(error))
|
||||
this.options.onError?.(error)
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 WebSocket 是否处于已连接状态
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isConnected() {
|
||||
if (!this.ws) return false
|
||||
// #ifdef APP-PLUS || MP-WEIXIN
|
||||
return this.ws.readyState === 1
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
return this.ws.readyState === WebSocket.OPEN
|
||||
// #endif
|
||||
}
|
||||
|
||||
/**
|
||||
* 方便外部获取连接状态码
|
||||
* @returns {number} 0:连接中,1:已连接,2:关闭中,3:已关闭
|
||||
*/
|
||||
getReadyState() {
|
||||
if (!this.ws) return 3
|
||||
// #ifdef APP-PLUS || MP-WEIXIN
|
||||
return this.ws.readyState
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
return this.ws.readyState
|
||||
// #endif
|
||||
}
|
||||
/**
|
||||
* 手动关闭 WebSocket 连接
|
||||
* 清理所有定时器、标记手动关闭、停止重连
|
||||
*/
|
||||
close() {
|
||||
console.log('手动关闭 WebSocket 连接')
|
||||
this.isManualClose = true
|
||||
this.stopHeartbeat()
|
||||
|
||||
// 清理重连定时器
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer)
|
||||
this.reconnectTimer = null
|
||||
}
|
||||
|
||||
if (this.ws) {
|
||||
// 移除所有事件监听,避免内存泄漏
|
||||
// #ifdef APP-PLUS || MP-WEIXIN
|
||||
if (this.ws.onOpen) this.ws.onOpen(null)
|
||||
if (this.ws.onMessage) this.ws.onMessage(null)
|
||||
if (this.ws.onError) this.ws.onError(null)
|
||||
if (this.ws.onClose) this.ws.onClose(null)
|
||||
// #endif
|
||||
|
||||
try {
|
||||
// #ifdef APP-PLUS || MP-WEIXIN
|
||||
this.ws.close({
|
||||
success: () => console.log('连接已关闭'),
|
||||
fail: (err) => console.error('关闭失败:', err)
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
this.ws.close()
|
||||
// #endif
|
||||
} catch (error) {
|
||||
console.error('关闭连接出错:', error)
|
||||
}
|
||||
this.ws = null
|
||||
}
|
||||
|
||||
// 重置重连计数
|
||||
this.reconnectAttempts = 0
|
||||
}
|
||||
/**
|
||||
* 更新配置项
|
||||
* 合并新配置,不覆盖原有配置
|
||||
* @param {Object} options - 新配置
|
||||
*/
|
||||
updateOptions(options) {
|
||||
this.options = {
|
||||
...this.options,
|
||||
...options
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// 单例实例化,全局唯一
|
||||
export const socketManager = new WebSocketManager()
|
||||
export default socketManager
|
||||
Reference in New Issue
Block a user