js realizes the encapsulation of WebSocket connection

First of all, if the project needs to interact with the client and server for a long time and without time and without interruption, using WebSocket connection is the best choice, such as the exchange project

So what functions are needed to create a WebSocket, and how can it be a robust ws system?

First we need several basic functions:

1. onopen (ws connection successful callback)

2. onmessage (ws returns data callback)

3. onclose (ws close callback)

4. onerror (ws error callback)

 When our ws has these methods, we need to add a few methods to interact with the server

1. subscribe

2. unSubscibe (unsubscribe)

3. request (send data to the server)

 With these methods, we need to consider some abnormal points

1. reConnect (ws reconnection, when ws is abnormal and disconnected, reconnect ws)

2. reSubscribe (resubscribe, when ws reconnects, resubscribe the previously subscribed events)

3. Heartbeat (ws heartbeat, we need to always check whether the server is still alive)

4. pollingRollback (polling backup callback, when ws is disconnected, the data still needs to be continuously updated)

 Now that we know this, we need to create some methods to implement it. I won’t talk about the logic of the specific implementation. You can directly look at the code.


 First, we create a socket folder, and create three files like this, so that we can maintain it easily

index.js

/**
 * websocket
 */
import Heartbeat from './heartbeat'
import PollingRollback from './pollingRollback'

export default class Socket {
  constructor(url) {
    this.ws = null
    this.url = url
    this.subscriptionMap = {}
    this.pollingRollback = null
    this.createPollingCallback() // 创建轮询
    this.start()
  }

  start() {
    if (!this.url) return console.error('url is required')
    this.ws = new WebSocket(this.url + "?lang=" + window.localStorage.lang);
    this.ws.addEventListener("open", this.onOpen);
    this.ws.addEventListener("message", this.onMessage);
    this.ws.addEventListener("close", this.onClose);
    this.ws.addEventListener("error", this.onError);
  }

  request(payload) { // 单纯地给服务器发送数据
    if (this.isConnected()) {
      this.ws.send(JSON.stringify({ ...payload, event: 'req' }));
    }
  }

  subscribe({ payload, rollback, callback }, isReSubscribe) {
    if (!isReSubscribe && this.subscriptionMap[payload.id]) return
    this.subscriptionMap[payload.id] = { payload, rollback, callback }
    this.pollingRollback.set(payload.id, rollback)

    if (this.isConnected()) {
      this.ws.send(JSON.stringify({ ...payload, event: 'sub' }));
    }
  }

  unSubscribe(id) {
    if (!id) return

    if (this.isConnected()) {
      if (this.subscriptionMap[id]) {
        const payload = this.subscriptionMap[id].payload
        this.ws.send(JSON.stringify({ ...payload, event: 'cancel' }));

        this.pollingRollback.remove(id)
        delete this.subscriptionMap[id];
      }
    }
  }

  isConnected() {
    return this.ws && this.ws.readyState === WebSocket.OPEN
  }

  onOpen = () => {
    clearInterval(this.reConnectTimer)
    this.createHeartbeat() // 创建 socket 心脏
    this.reSubscribe() // 重新订阅已有的sub
    this.pollingRollback.close() // ws 连接之后,关闭轮询
  }

  onMessage = (result) => {
    const data = result.data
    if (/ping|pong/i.test(data)) return

    const normalizedData = JSON.parse(data || "{}");
    this.handleCallback(normalizedData)
  }

  handleCallback = (data) => {
    const id = data.id;
    if (!id) return;

    if (this.subscriptionMap[id]) {
      this.subscriptionMap[id]["callback"] && this.subscriptionMap[id]["callback"](data);
    }
  }

  onClose = () => {
    console.warn(`【Websocket is closed】`)
    this.ws.removeEventListener("open", this.onOpen);
    this.ws.removeEventListener("message", this.onMessage);
    this.ws.removeEventListener("close", this.onClose);
    this.ws.removeEventListener("error", this.onError);
    this.ws = null;
  }

  onError = (error) => {
    if (error && error.message) {
      console.error(`【Websocket error】 ${error.message}`)
    }
    this.ws.close()
    this.reConnect()
  }

  reConnect() { // 开启重连
    this.pollingRollback.open() // ws连接之前,开启轮询

    if (this.reConnectTimer) return
    this.reConnectTimer = setInterval(() => {
      this.start()
    }, 3000)
  }

  reSubscribe() {
    Object.values(this.subscriptionMap).forEach(subscription => this.subscribe(subscription, true))
  }

  createHeartbeat() {
    this.heartbeat = new Heartbeat(this.ws)
    this.heartbeat.addEventListener('die', () => {
      this.ws.close()
      this.ws.reConnect()
    })
  }

  createPollingCallback() {
    this.pollingRollback = new PollingRollback()
  }
}


heartbeat.js

/**
 * 心跳
 */
const INTERVAL = 5000 // ping 的间隔
const TIMEOUT = INTERVAL * 2 // 超时时间(只能是INTERVAL的整数倍数,超过这个时间会触发心跳死亡事件) 默认为 ping 两次没有响应则超时
const DIE_EVENT = new CustomEvent("die") // 心跳死亡事件 => 超时时触发

export default class Heartbeat extends EventTarget {
  constructor(ws, interval, timeout) {
    super()
    if (!ws) return

    this.ws = ws
    this.interval = interval || INTERVAL
    this.timeout = timeout || TIMEOUT
    this.counter = 0

    this.ws.addEventListener("message", this.onMessage)
    this.ws.addEventListener("close", this.onClose)
    this.start()
  }

  ping() {
    this.counter += 1
    if (this.counter > (this.timeout / this.interval)) { // ping 没有响应 pong
      this.dispatchEvent(DIE_EVENT)
      return
    }

    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      const data = JSON.stringify({ ping: new Date().getTime() })
      this.ws.send(data);
    }
  }

  pong(data) {
    this.ws.send(data.replace("ping", "pong"));
  }

  onMessage = (result) => {
    const data = result.data;

    if (/pong/i.test(data)) { // 服务器响应重新计数
      return this.counter = 0
    }

    if (/ping/.test(data)) { // 服务器 ping 我们
      return this.pong(data)
    }
  }

  onClose = () => {
    this.ws.removeEventListener("message", this.onMessage);
    this.ws.removeEventListener("close", this.onClose);
    this.ws = null;
    clearInterval(this.keepAliveTimer)
  }

  start() {
    this.keepAliveTimer = setInterval(this.ping, this.interval)
  }
}

pollingRollback.js

/**
 * 轮询
 */

export default class PollingRollback {
  constructor(interval) {
    this.rollbackMap = {}
    this.rollbackTimer = null
    this.interval = interval || 3000
  }

  set(id, rollback) {
    this.rollbackMap[id] = rollback
  }

  remove(id) {
    delete this.rollbackMap[id]
  }

  open() {
    this.rollbackTimer = setInterval(() => {
      Object.values(this.rollbackMap).forEach(rollback => rollback && rollback())
    }, this.interval)
  }

  close() {
    clearInterval(this.rollbackTimer)
  }
}

In this way, a complete Socket is encapsulated, so how should we use it?

We can create a ws.js file and encapsulate the method we need into a class again.
As for some people asking why we need to encapsulate a class on the original basis, because the exposed method must be brief and clear, and the original class can also be used directly. If only two people develop it, it will not be easy for the other person to get started!

ws.js

import Socket from './socket'

class CommonWs {
  constructor(url) {
    this.ws = null
    this.url = url
  }

  connect() {
    this.ws = new Socket(this.url)
  }

  request(payload) {
    if (!this.ws) this.connect()
    this.ws.request(payload)
  }

  subscribe(payload, rollback, callback) {
    if (!this.ws) this.connect()
    this.ws.subscribe({ payload, rollback, callback })
  }

  unSubscribe(id) {
    if (!this.ws) this.connect()
    this.ws.unSubscribe(id)
  }

  close() {
    if (!this.ws) return
    this.ws.close()
  }

  isConnected() {
    return this.ws && this.ws.isConnected()
  }
}

// ws
export const ws = new CommonWs(<wsUrl>)

// ws2
export const ws2 = new CommonWs(<ws2Url>)

 Ok, at this point you can directly create ws to use methods such as subscription, unsubscription, closing ws connection, etc. As for the things behind the Socket, we don't need to know, as long as our code is bug-free.

Here I add the meaning of the subscribe parameter

* payload Subscription required content, for example, if we want to subscribe to wallet balance information, the background needs us to send id, then we will pass { id: 'balance' } and the like

* rollback During the abnormal disconnection of the ws connection, the data on the page still needs to be refreshed, so we use the rollback function to poll and update the data

* callback As the name suggests, the corresponding method will be called after ws returns the data

Guess you like

Origin blog.csdn.net/weixin_42335036/article/details/118214773