websocket.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. import env from '../config/env.js'
  2. const HEARTBEAT_CMD = { cmd: 1 }
  3. function safeJsonParse(data) {
  4. if (typeof data !== 'string') return data
  5. try {
  6. return JSON.parse(data)
  7. } catch (error) {
  8. return data
  9. }
  10. }
  11. function getHostFromBaseUrl(baseUrl) {
  12. return String(baseUrl || '')
  13. .replace(/^https?:\/\//, '')
  14. .replace(/\/+$/, '')
  15. }
  16. class WebSocketService {
  17. constructor() {
  18. this.socketTask = null
  19. this.isConnected = false
  20. this.isManualClose = false
  21. this.connectingPromise = null
  22. this.connectOptions = null
  23. this.reconnectAttempts = 0
  24. this.maxReconnectAttempts = 5
  25. this.reconnectInterval = 3000
  26. this.reconnectTimer = null
  27. this.heartbeatInterval = 20000
  28. this.heartbeatTimer = null
  29. this.listeners = {}
  30. this.useGlobalSocketApi = false
  31. this.globalSocketHandlers = null
  32. }
  33. on(eventName, handler) {
  34. if (!eventName || typeof handler !== 'function') return () => {}
  35. if (!this.listeners[eventName]) this.listeners[eventName] = new Set()
  36. this.listeners[eventName].add(handler)
  37. return () => this.off(eventName, handler)
  38. }
  39. off(eventName, handler) {
  40. if (!eventName || !this.listeners[eventName]) return
  41. if (!handler) {
  42. this.listeners[eventName].clear()
  43. return
  44. }
  45. this.listeners[eventName].delete(handler)
  46. }
  47. emit(eventName, payload) {
  48. const group = this.listeners[eventName]
  49. if (!group || !group.size) return
  50. group.forEach((fn) => {
  51. try {
  52. fn(payload)
  53. } catch (error) {
  54. console.error(`[WebSocket] listener error for ${eventName}:`, error)
  55. }
  56. })
  57. }
  58. buildSocketUrl(options = {}) {
  59. const role = options.role || 'parent'
  60. const base = 'wss://yx.newfeifan.cn/btcSkt'
  61. // 开发地址(保留注释):const base = 'ws://192.168.220.13:8080/btcSkt'
  62. // 旧线上地址(保留注释):const base = 'wss://m.hfdcschool.com/btcSkt'
  63. // 原 env 写法(保留注释):const base = `wss://${getHostFromBaseUrl(env.baseUrl)}/btcSkt`
  64. if (role === 'device') {
  65. const ssDev = options.ssDev || ''
  66. if (!ssDev) throw new Error('缺少 ssDev')
  67. return `${base}?ssDev=${encodeURIComponent(ssDev)}`
  68. }
  69. const ssToken = options.ssToken || ''
  70. if (!ssToken) throw new Error('缺少 ssToken')
  71. return `${base}?ssToken=${encodeURIComponent(ssToken)}`
  72. }
  73. async connect(options = {}) {
  74. if (this.isConnected && this.socketTask && !options.forceReconnect) {
  75. return true
  76. }
  77. if (this.connectingPromise) {
  78. return this.connectingPromise
  79. }
  80. if (options.forceReconnect) {
  81. await this.disconnect()
  82. }
  83. const merged = {
  84. role: options.role || this.connectOptions?.role || 'parent',
  85. ssToken: options.ssToken || this.connectOptions?.ssToken || '',
  86. ssDev: options.ssDev || this.connectOptions?.ssDev || '',
  87. heartbeat: options.heartbeat ?? this.connectOptions?.heartbeat ?? false,
  88. heartbeatInterval: options.heartbeatInterval || this.connectOptions?.heartbeatInterval || this.heartbeatInterval,
  89. autoReconnect: options.autoReconnect ?? this.connectOptions?.autoReconnect ?? true,
  90. maxReconnectAttempts: options.maxReconnectAttempts || this.connectOptions?.maxReconnectAttempts || this.maxReconnectAttempts,
  91. reconnectInterval: options.reconnectInterval || this.connectOptions?.reconnectInterval || this.reconnectInterval
  92. }
  93. this.connectOptions = merged
  94. this.heartbeatInterval = merged.heartbeatInterval
  95. this.maxReconnectAttempts = merged.maxReconnectAttempts
  96. this.reconnectInterval = merged.reconnectInterval
  97. this.isManualClose = false
  98. const wsUrl = this.buildSocketUrl(merged)
  99. const socketApi = typeof wx !== 'undefined' ? wx : uni
  100. this.connectingPromise = new Promise((resolve, reject) => {
  101. let settled = false
  102. let openTimeout = null
  103. const resolveOnce = (value) => {
  104. if (settled) return
  105. settled = true
  106. resolve(value)
  107. }
  108. const rejectOnce = (error) => {
  109. if (settled) return
  110. settled = true
  111. reject(error)
  112. }
  113. const handleOpen = () => {
  114. if (openTimeout) clearTimeout(openTimeout)
  115. this.isConnected = true
  116. this.reconnectAttempts = 0
  117. this.connectingPromise = null
  118. if (merged.heartbeat) {
  119. this.startHeartbeat()
  120. } else {
  121. this.stopHeartbeat()
  122. }
  123. this.emit('open', { url: wsUrl, options: merged })
  124. resolveOnce(true)
  125. }
  126. const handleMessage = (res) => {
  127. const data = safeJsonParse(res?.data)
  128. this.emit('message', data)
  129. if (data && typeof data === 'object' && data.cmd !== undefined) {
  130. this.emit(`cmd:${data.cmd}`, data)
  131. if (data.cmd === 51) this.emit('refresh', data)
  132. }
  133. }
  134. const handleError = (error) => {
  135. this.emit('error', error)
  136. if (!this.isConnected && this.connectingPromise) {
  137. this.connectingPromise = null
  138. rejectOnce(error)
  139. }
  140. }
  141. const handleClose = (res) => {
  142. this.isConnected = false
  143. this.stopHeartbeat()
  144. this.emit('close', res)
  145. if (!this.isManualClose && this.connectOptions?.autoReconnect) {
  146. this.tryReconnect()
  147. }
  148. }
  149. let task = null
  150. try {
  151. task = socketApi.connectSocket({ url: wsUrl })
  152. } catch (error) {
  153. this.connectingPromise = null
  154. rejectOnce(error)
  155. return
  156. }
  157. if (task && typeof task.onOpen === 'function') {
  158. this.useGlobalSocketApi = false
  159. this.socketTask = task
  160. task.onOpen(handleOpen)
  161. task.onMessage(handleMessage)
  162. task.onError(handleError)
  163. task.onClose(handleClose)
  164. return
  165. }
  166. if (
  167. typeof socketApi.onSocketOpen !== 'function' ||
  168. typeof socketApi.onSocketMessage !== 'function' ||
  169. typeof socketApi.onSocketError !== 'function' ||
  170. typeof socketApi.onSocketClose !== 'function'
  171. ) {
  172. this.connectingPromise = null
  173. rejectOnce(new Error('创建 WebSocket 失败'))
  174. return
  175. }
  176. this.useGlobalSocketApi = true
  177. this.socketTask = { __global: true }
  178. const openHandler = () => handleOpen()
  179. const messageHandler = (res) => handleMessage(res)
  180. const errorHandler = (err) => handleError(err)
  181. const closeHandler = (res) => handleClose(res)
  182. this.globalSocketHandlers = { openHandler, messageHandler, errorHandler, closeHandler }
  183. socketApi.onSocketOpen(openHandler)
  184. socketApi.onSocketMessage(messageHandler)
  185. socketApi.onSocketError(errorHandler)
  186. socketApi.onSocketClose(closeHandler)
  187. openTimeout = setTimeout(() => {
  188. this.connectingPromise = null
  189. rejectOnce(new Error('WebSocket 连接超时(未收到 onSocketOpen)'))
  190. }, 15000)
  191. })
  192. return this.connectingPromise
  193. }
  194. tryReconnect() {
  195. if (this.reconnectTimer) return
  196. if (this.reconnectAttempts >= this.maxReconnectAttempts) return
  197. this.reconnectAttempts += 1
  198. this.reconnectTimer = setTimeout(async () => {
  199. this.reconnectTimer = null
  200. try {
  201. await this.connect(this.connectOptions || {})
  202. } catch (error) {
  203. this.tryReconnect()
  204. }
  205. }, this.reconnectInterval)
  206. }
  207. startHeartbeat() {
  208. this.stopHeartbeat()
  209. this.heartbeatTimer = setInterval(() => {
  210. if (!this.isConnected) return
  211. this.send(HEARTBEAT_CMD).catch(() => {})
  212. }, this.heartbeatInterval)
  213. }
  214. stopHeartbeat() {
  215. if (!this.heartbeatTimer) return
  216. clearInterval(this.heartbeatTimer)
  217. this.heartbeatTimer = null
  218. }
  219. async disconnect() {
  220. this.isManualClose = true
  221. this.stopHeartbeat()
  222. if (this.reconnectTimer) {
  223. clearTimeout(this.reconnectTimer)
  224. this.reconnectTimer = null
  225. }
  226. const socketApi = typeof wx !== 'undefined' ? wx : uni
  227. return new Promise((resolve) => {
  228. if (!this.socketTask) {
  229. this.socketTask = null
  230. this.useGlobalSocketApi = false
  231. this.isConnected = false
  232. this.connectingPromise = null
  233. resolve()
  234. return
  235. }
  236. if (this.useGlobalSocketApi || this.socketTask.__global) {
  237. if (this.globalSocketHandlers) {
  238. const { openHandler, messageHandler, errorHandler, closeHandler } = this.globalSocketHandlers
  239. if (typeof socketApi.offSocketOpen === 'function') socketApi.offSocketOpen(openHandler)
  240. if (typeof socketApi.offSocketMessage === 'function') socketApi.offSocketMessage(messageHandler)
  241. if (typeof socketApi.offSocketError === 'function') socketApi.offSocketError(errorHandler)
  242. if (typeof socketApi.offSocketClose === 'function') socketApi.offSocketClose(closeHandler)
  243. this.globalSocketHandlers = null
  244. }
  245. socketApi.closeSocket({
  246. complete: () => {
  247. this.socketTask = null
  248. this.useGlobalSocketApi = false
  249. this.isConnected = false
  250. this.connectingPromise = null
  251. resolve()
  252. }
  253. })
  254. return
  255. }
  256. if (typeof this.socketTask.close !== 'function') {
  257. this.socketTask = null
  258. this.useGlobalSocketApi = false
  259. this.isConnected = false
  260. this.connectingPromise = null
  261. resolve()
  262. return
  263. }
  264. this.socketTask.close({
  265. complete: () => {
  266. this.socketTask = null
  267. this.useGlobalSocketApi = false
  268. this.isConnected = false
  269. this.connectingPromise = null
  270. resolve()
  271. }
  272. })
  273. })
  274. }
  275. async send(payload) {
  276. if (!this.socketTask || !this.isConnected) {
  277. throw new Error('WebSocket 未连接')
  278. }
  279. const socketApi = typeof wx !== 'undefined' ? wx : uni
  280. return new Promise((resolve, reject) => {
  281. if (this.useGlobalSocketApi || this.socketTask.__global) {
  282. socketApi.sendSocketMessage({
  283. data: JSON.stringify(payload),
  284. success: () => resolve(true),
  285. fail: (error) => reject(error)
  286. })
  287. return
  288. }
  289. if (typeof this.socketTask.send !== 'function') {
  290. reject(new Error('WebSocket 发送不可用'))
  291. return
  292. }
  293. this.socketTask.send({
  294. data: JSON.stringify(payload),
  295. success: () => resolve(true),
  296. fail: (error) => reject(error)
  297. })
  298. })
  299. }
  300. async ensureConnected(options = {}) {
  301. if (this.isConnected && this.socketTask) return true
  302. await this.connect(options)
  303. return true
  304. }
  305. }
  306. const websocketService = new WebSocketService()
  307. export default websocketService