本封装方式只是真对目前自己所在公司当下业务需求,后期可能会修改,本文仅仅只作为学习记录。

使用方式

主要使用的方式有4种,建立连接 io()、关闭连接 WS.close()、发送信息使用 WS.send() / io().send() 、监听 WS.on() / io().on()

  1. import { WSMessageTypeMap, WS, io } from '@/utils/webSocket'
  2. // ---------------------建立连接 -----------------------------
  3. io({...})
  4. // ---------------------关闭连接 -----------------------------
  5. WS.close() 或者 WS.close(()=>{}) // 支持回调
  6. // ---------------------发送 -----------------------------
  7. WS.send({
  8. type:WSMessageTypeMap['订单状态通知'], // type:必填
  9. data?:{}
  10. },function (){
  11. //回调方法
  12. })
  13. // ---------------------监听 -----------------------------
  14. WS.on(WSMessageTypeMap['订单状态通知'],function (){
  15. //回调方法
  16. })
  17. // 或着
  18. WS.on(WSMessageTypeMap['订单状态通知'],[function (){
  19. //回调方法
  20. }])

封装过程

WebSocketClient

用于创建 webSocket 实例,并建立连接。

HeartBeat

用于心跳检测,主动向后端发送消息,当一定时间内没有收到后端恢复,就会断开重连。

最终结果:

  1. // 业务类型:0-心跳检测;1-订单状态通知;2-接驾路线通知;3-服务中订单查询。。。。后期会组件扩展增多
  2. export const WSMessageTypeMap = {
  3. HeartBeat: 0,
  4. 订单状态通知: 1,
  5. 接驾路线通知: 2,
  6. 服务中订单查询: 3,
  7. 司机到达时间: 4,
  8. }
  9. const replaceUrl = (url, opts) => {
  10. url += '/websocket?'
  11. for (const key in opts) {
  12. // HeartBeatTimeout 心跳时间不处理
  13. if (opts.hasOwnProperty(key) && key != 'HeartBeatTimeout') {
  14. url = url + `${key}=${opts[key]}&`
  15. }
  16. }
  17. // console.log('🚀 ~ file: webSocket.js ~ line 10 ~ replaceUrl ~ url', url)
  18. // TODO: 需要支持wss,ws明文传输在生产环境太危险了
  19. url = url
  20. .slice(0, -1)
  21. .split('//')
  22. .join('/')
  23. .replace('https:/', 'wss://')
  24. .replace('http:/', 'ws://')
  25. return url
  26. }
  27. /**
  28. * HeartBeat 心跳检测,防止websocket中途断开,断开后重连
  29. * @param {object} websocketClient websocketClient实例
  30. * @param {number} timeout 心跳时间,默认10s
  31. */
  32. class HeartBeat {
  33. _timeout = 10000
  34. _timeoutObj = null
  35. _serverTimeoutObj = null
  36. _websocketClient = null
  37. constructor(websocketClient, timeout) {
  38. this._websocketClient = websocketClient
  39. this._timeout = timeout || this._timeout
  40. this.reset()
  41. this.start()
  42. }
  43. reset() {
  44. clearTimeout(this._timeoutObj)
  45. clearTimeout(this._serverTimeoutObj)
  46. return this
  47. }
  48. start() {
  49. const self = this
  50. this._timeoutObj && clearTimeout(this._timeoutObj)
  51. this._serverTimeoutObj && clearTimeout(this._serverTimeoutObj)
  52. this._timeoutObj = setTimeout(function () {
  53. // 这里发送一个心跳,后端收到后,返回一个心跳消息,
  54. // onmessage拿到返回的心跳就说明连接正常
  55. self._websocketClient.send({ type: WSMessageTypeMap['HeartBeat'] })
  56. self._serverTimeoutObj = setTimeout(function () {
  57. // console.log('🚀 ~ file: webSocket.js ~ line 61 ~ HeartBeat ~ setTimeout', '主动关闭连接')
  58. // 如果超过一定时间还没重置,说明后端主动断开了
  59. self._websocketClient.close() // 如果onclose会执行reconnect,我们执行close()就行了.如果直接执行 reconnect 会触发onclose导致重连两次
  60. }, self._timeout)
  61. }, self._timeout)
  62. }
  63. }
  64. /**
  65. * @param {string} url 指定创建连接的地址,默认为 process.env.BASE_API
  66. * @param {object} opts 指定配置项
  67. */
  68. export class WebSocketClient {
  69. _websocket
  70. _token
  71. _path // 路径
  72. _opts
  73. _readyState
  74. _isConnected // 是否连接
  75. openList = [] // 连接成功回调队列
  76. wsCallbackMap = new Map() // 回调Map
  77. closewebsocketCallback = () => {} // websocket 关闭回调函数
  78. _lockReconnect = false // 避免重复连接
  79. _tt
  80. HeartBeat = null // 心跳检测对象
  81. constructor(opts, url) {
  82. if (WS) return WS
  83. this._opts = opts
  84. this.createWebSocket(url)
  85. }
  86. createWebSocket(url) {
  87. if (this._websocket) {
  88. return this._websocket
  89. }
  90. this._path =
  91. this._path || replaceUrl(url || process.env.BASE_API, this._opts)
  92. try {
  93. if ('WebSocket' in window) {
  94. this._websocket = new WebSocket(this._path)
  95. } else if ('MozWebSocket' in window) {
  96. // Note: 新版的FirFox已经没有这个构造函数了 99.0 (64 位)
  97. // eslint-disable-next-line no-undef
  98. this._websocket = new MozWebSocket(this._path)
  99. }
  100. this.init()
  101. } catch (e) {
  102. this.reconnect()
  103. // eslint-disable-next-line no-console
  104. console.error('createWebSocket error', e)
  105. }
  106. }
  107. /**
  108. * 初始化
  109. */
  110. init() {
  111. this.HeartBeat =
  112. this.HeartBeat || new HeartBeat(this, this._opts.HeartBeatTimeout)
  113. // websocket 连接成功
  114. this._websocket.onopen = () => {
  115. // console.log('🚀 ~ file: webSocket.js ~ line 104 ~ WebSocketClient ~ init ~ onopen',)
  116. this._isConnected = true
  117. // 执行连接成功的回调
  118. const f = this.openList.shift()
  119. typeof f === 'function' && f()
  120. }
  121. this._websocket.onmessage = (event) => {
  122. const data = JSON.parse(event.data)
  123. if (
  124. process.env.NODE_ENV === 'development' &&
  125. data.type !== WSMessageTypeMap['HeartBeat']
  126. ) {
  127. // eslint-disable-next-line no-console
  128. console.log(
  129. '🚀 ~ file: webSocket.js ~ line 111 ~ WebSocketClient ~ init ~ onmessage',
  130. data
  131. )
  132. }
  133. // 重启心跳检测
  134. this.HeartBeat.reset().start()
  135. // 执行所有回调
  136. const wsCallbackMap = this.wsCallbackMap.get(data.type)
  137. wsCallbackMap && wsCallbackMap.forEach((callback) => callback(data.data))
  138. }
  139. this._websocket.onclose = () => {
  140. // console.log('🚀 ~ file: webSocket.js ~ line 113 ~ WebSocketClient ~ init ~ onclose',)
  141. this._isConnected = false
  142. this.HeartBeat.reset()
  143. this.reconnect()
  144. }
  145. this._websocket.onerror = () => {
  146. // console.log('🚀 ~ file: webSocket.js ~ line 118 ~ WebSocketClient ~ init ~ onerror')
  147. this._isConnected = false
  148. this.reconnect()
  149. }
  150. // 其他异常情况处理
  151. // 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
  152. window.onbeforeunload = function () {
  153. this.close()
  154. }
  155. // 处理手机手机熄屏后会中断
  156. document.addEventListener('visibilitychange', () => {
  157. let hiddenTime
  158. if (document.visibilityState == 'hidden') {
  159. // 记录页面隐藏时间
  160. hiddenTime = new Date().getTime()
  161. } else {
  162. const visibleTime = new Date().getTime()
  163. // 页面再次可见的时间-隐藏时间>10S,重连
  164. if ((visibleTime - hiddenTime) / 1000 > 10) {
  165. // 主动关闭连接
  166. this.close()
  167. // 1.5S后重连 因为断开需要时间,防止连接早已关闭了
  168. setTimeout(() => {
  169. this.reconnect()
  170. }, 1500)
  171. } else {
  172. //
  173. }
  174. }
  175. })
  176. }
  177. /**
  178. * 收集回调函数,当收到服务器消息时,会执行对应的回调函数
  179. * @param {*} callback 回调函数,可以是一个回调函数队列数组
  180. * @param {*} method 回调队列名称,对应前后的交互数据的 type
  181. */
  182. on(method, callback) {
  183. const tm = this.wsCallbackMap.get(method)
  184. if (tm) {
  185. Array.isArray(callback) ? tm.push(...callback) : tm.push(callback)
  186. } else {
  187. const t = Array.isArray(callback) ? [...callback] : [callback]
  188. this.wsCallbackMap.set(method, t)
  189. }
  190. }
  191. /**
  192. * 删除回调函数
  193. * @param {*} method 回调队列名称,对应前后的交互数据的 type
  194. */
  195. removeOn(method) {
  196. this.wsCallbackMap.delete(method)
  197. }
  198. /**
  199. * 主动向后端发送消息
  200. * @param {*} data 发送的数据 格式必须为: {type:1,data:{}},type为必须的字段,因为是根据type来收集回调函数
  201. * @param {Functon} callback 发送成功后后端返回数据执行回调函数
  202. * data : {
  203. * type: 0, // WSMessageTypeMap[key]
  204. * data: {} // 需要发送的数据
  205. * }
  206. */
  207. send(data, callback = () => { }) {
  208. const cheapFunction = () => {
  209. data.data = { ...data.data }
  210. if (data.type !== WSMessageTypeMap['HeartBeat']) {
  211. this.removeOn(`send_${data.type}`) // 删除回调函数,必须先删除,否则会导致多次执行
  212. this.on(`send_${data.type}`, callback)
  213. }
  214. this._websocket.send(JSON.stringify(data))
  215. if (data.type !== WSMessageTypeMap['HeartBeat']) {
  216. // 重启心跳检测 方式发送不是心态检测内容时候,没有收到信息,导致心跳检测失效
  217. this.HeartBeat.reset()
  218. }
  219. }
  220. // 如果连接成功,直接发送
  221. if (this._isConnected) {
  222. cheapFunction()
  223. } else {
  224. // 添加在openList 延迟执行
  225. this.openList.push(() => {
  226. setTimeout(() => {
  227. cheapFunction()
  228. }, 1000)
  229. })
  230. }
  231. }
  232. /**
  233. * 关闭连接
  234. */
  235. close(closewebsocketCallback) {
  236. this.closewebsocketCallback =
  237. closewebsocketCallback || this.closewebsocketCallback
  238. this._websocket.close()
  239. this._lockReconnect = true
  240. this._isConnected = false
  241. this._path = undefined
  242. this._websocket = null
  243. this.closewebsocketCallback()
  244. }
  245. /**
  246. * 重连
  247. * @returns
  248. */
  249. reconnect() {
  250. // console.log('🚀 ~ file: webSocket.js ~ line 185 ~ WebSocketClient ~ reconnect ~ reconnect')
  251. if (this._isConnected) return
  252. this._lockReconnect = true
  253. this.tt && clearTimeout(this.tt)
  254. // 没连接上会一直重连,设置延迟避免请求过多
  255. this.tt = setTimeout(() => {
  256. // console.log('🚀 ~ file: webSocket.js ~ line 194 ~ WebSocketClient ~ this.tt=setTimeout ~ setTimeout',)
  257. this._lockReconnect = false
  258. this._websocket = null
  259. this.createWebSocket()
  260. }, 4000)
  261. }
  262. }
  263. let WS = null
  264. /**
  265. * 默认创建websocketClient 方法
  266. * @param {*} opts 配置对象
  267. */
  268. const io = (opts) => {
  269. // 一定要判断是否已经创建过WebSocketClient,否则会导致重复创建
  270. if (WS) return WS
  271. const userInfoEtravel = {
  272. ...JSON.parse(sessionStorage.getItem('userInfoEtravel')),
  273. }
  274. opts = {
  275. token: userInfoEtravel.token,
  276. ID: userInfoEtravel.staffId,
  277. HeartBeatTimeout: 3000,
  278. ...opts,
  279. }
  280. WS = new WebSocketClient(opts)
  281. return WS
  282. }
  283. /**
  284. *
  285. * @param {Object} WS WS 为 WebSocketClient 实例,可以通过创建 io() 获取
  286. * @param {Function} io io 为 默认 创建 WebSocketClient 实例的方法,可自定义自己的创建方法
  287. *
  288. *
  289. // ---------------------建立连接 -----------------------------
  290. io({...})
  291. // ---------------------关闭连接 -----------------------------
  292. WS.close() 或者 WS.close(()=>{}) // 支持回调
  293. // ---------------------发送 -----------------------------
  294. WS.send({
  295. type:WSMessageTypeMap['订单状态通知'], // type:必填
  296. data?:{}
  297. },function (){
  298. //回调方法
  299. })
  300. // ---------------------监听 -----------------------------
  301. WS.on(WSMessageTypeMap['订单状态通知'],function (){
  302. //回调方法
  303. })
  304. // 或着
  305. WS.on(WSMessageTypeMap['订单状态通知'],[function (){
  306. //回调方法
  307. }])
  308. *
  309. */
  310. export { WS, io }