You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

219 lines
5.1 KiB

3 months ago
3 months ago
3 months ago
  1. /**
  2. * WebSocket 工具类
  3. *
  4. * 功能说明
  5. * 1. 封装WebSocket连接管理
  6. * 2. 自动重连机制
  7. * 3. 心跳检测
  8. * 4. 订阅管理
  9. *
  10. * 使用示例
  11. * import WebSocketClient from '@/utils/websocket'
  12. *
  13. * // 连接
  14. * WebSocketClient.connect('http://localhost:8080/ws/dashboard', () => {
  15. * console.log('连接成功')
  16. * // 订阅主题
  17. * WebSocketClient.subscribe('/topic/dashboard/robot-picking', (data) => {
  18. * console.log('收到数据:', data)
  19. * })
  20. * })
  21. */
  22. import SockJS from 'sockjs-client'
  23. import Stomp from 'stompjs'
  24. class WebSocketClient {
  25. constructor() {
  26. this.stompClient = null
  27. this.connected = false
  28. this.reconnectAttempts = 0
  29. this.maxReconnectAttempts = 3000
  30. this.reconnectInterval = 10000
  31. this.subscriptions = new Map()
  32. this.heartbeatInterval = null
  33. this.url = null
  34. this.onConnectedCallback = null
  35. }
  36. /**
  37. * 连接WebSocket服务器
  38. *
  39. * @param {string} url WebSocket服务器地址
  40. * @param {function} onConnected 连接成功回调
  41. * @param {function} onError 连接错误回调
  42. */
  43. connect(url, onConnected, onError) {
  44. this.url = url
  45. this.onConnectedCallback = onConnected
  46. console.log('[WebSocket] 正在连接服务器...', url)
  47. try {
  48. const socket = new SockJS(url)
  49. this.stompClient = Stomp.over(socket)
  50. // 禁用调试日志(生产环境)
  51. // 开发环境可以设置为 console.log
  52. this.stompClient.debug = null
  53. this.stompClient.connect(
  54. {},
  55. frame => {
  56. console.log('[WebSocket] ✅ 连接成功')
  57. this.connected = true
  58. this.reconnectAttempts = 0
  59. this.startHeartbeat()
  60. if (onConnected) {
  61. onConnected(frame)
  62. }
  63. },
  64. error => {
  65. console.error('[WebSocket] ❌ 连接失败:', error)
  66. this.connected = false
  67. this.handleDisconnect()
  68. if (onError) {
  69. onError(error)
  70. }
  71. }
  72. )
  73. } catch (error) {
  74. console.error('[WebSocket] 连接异常:', error)
  75. if (onError) {
  76. onError(error)
  77. }
  78. }
  79. }
  80. /**
  81. * 订阅主题
  82. *
  83. * @param {string} topic 主题名称
  84. * @param {function} callback 消息回调函数
  85. * @returns {string} 订阅ID
  86. */
  87. subscribe(topic, callback) {
  88. if (!this.connected || !this.stompClient) {
  89. console.warn('[WebSocket] 未连接,无法订阅主题:', topic)
  90. return null
  91. }
  92. console.log('[WebSocket] 订阅主题:', topic)
  93. const subscription = this.stompClient.subscribe(topic, message => {
  94. try {
  95. const data = JSON.parse(message.body)
  96. callback(data)
  97. } catch (error) {
  98. console.error('[WebSocket] 解析消息失败:', error)
  99. }
  100. })
  101. const subscriptionId = topic
  102. this.subscriptions.set(subscriptionId, subscription)
  103. return subscriptionId
  104. }
  105. /**
  106. * 取消订阅
  107. *
  108. * @param {string} subscriptionId 订阅ID
  109. */
  110. unsubscribe(subscriptionId) {
  111. const subscription = this.subscriptions.get(subscriptionId)
  112. if (subscription) {
  113. subscription.unsubscribe()
  114. this.subscriptions.delete(subscriptionId)
  115. console.log('[WebSocket] 取消订阅:', subscriptionId)
  116. }
  117. }
  118. /**
  119. * 发送消息
  120. *
  121. * @param {string} destination 目标地址
  122. * @param {object} data 消息数据
  123. */
  124. send(destination, data) {
  125. if (!this.connected || !this.stompClient) {
  126. console.warn('[WebSocket] 未连接,无法发送消息')
  127. return
  128. }
  129. this.stompClient.send(destination, {}, JSON.stringify(data))
  130. }
  131. /**
  132. * 断开连接
  133. */
  134. disconnect() {
  135. if (this.stompClient) {
  136. this.stompClient.disconnect(() => {
  137. console.log('[WebSocket] 已断开连接')
  138. })
  139. this.connected = false
  140. this.stopHeartbeat()
  141. }
  142. }
  143. /**
  144. * 处理断开连接
  145. */
  146. handleDisconnect() {
  147. this.connected = false
  148. this.stopHeartbeat()
  149. // 自动重连
  150. if (this.reconnectAttempts < this.maxReconnectAttempts) {
  151. this.reconnectAttempts++
  152. console.log(`[WebSocket] 尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`)
  153. setTimeout(() => {
  154. if (this.url && this.onConnectedCallback) {
  155. this.connect(this.url, this.onConnectedCallback)
  156. }
  157. }, this.reconnectInterval)
  158. } else {
  159. console.error('[WebSocket] 达到最大重连次数,放弃重连')
  160. }
  161. }
  162. /**
  163. * 启动心跳检测
  164. */
  165. startHeartbeat() {
  166. this.heartbeatInterval = setInterval(() => {
  167. if (this.connected && this.stompClient) {
  168. // 发送心跳消息
  169. // 注意:如果后端没有心跳接口,可以注释掉这行
  170. // this.send('/app/heartbeat', { timestamp: Date.now() })
  171. }
  172. }, 30000) // 每30秒发送一次心跳
  173. }
  174. /**
  175. * 停止心跳检测
  176. */
  177. stopHeartbeat() {
  178. if (this.heartbeatInterval) {
  179. clearInterval(this.heartbeatInterval)
  180. this.heartbeatInterval = null
  181. }
  182. }
  183. /**
  184. * 检查连接状态
  185. *
  186. * @returns {boolean} 是否已连接
  187. */
  188. isConnected() {
  189. return this.connected
  190. }
  191. }
  192. // 导出单例
  193. export default new WebSocketClient()