js realiza la encapsulación de la conexión WebSocket

En primer lugar, si el proyecto necesita interactuar con el cliente y el servidor durante mucho tiempo, sin tiempo y sin interrupción, utilizar la conexión WebSocket es la mejor opción, como el proyecto de intercambio.

Entonces, ¿qué funciones se necesitan para crear un WebSocket y cómo puede ser un sistema ws robusto?

Primero necesitamos varias funciones básicas:

1. onopen (devolución de llamada exitosa de la conexión ws)

2. onmessage (ws devuelve la devolución de llamada de datos)

3. onclose (ws cierre de devolución de llamada)

4. onerror (devolución de llamada de error ws)

 Cuando nuestro ws tiene estos métodos, necesitamos agregar algunos métodos para interactuar con el servidor

1. suscríbete

2. darse de baja (darse de baja)

3. solicitud (enviar datos al servidor)

 Con estos métodos, necesitamos considerar algunos puntos anormales

1. reconectar (reconexión de ws, cuando ws es anormal y está desconectado, reconectar ws)

2. reSubscribe (resuscribe, cuando ws se vuelve a conectar, vuelve a suscribir los eventos suscritos previamente)

3. Heartbeat (ws heartbeat, siempre debemos verificar si el servidor aún está vivo)

4. pollingRollback (devolución de llamada de respaldo de sondeo, cuando ws está desconectado, los datos aún deben actualizarse continuamente)

 Ahora que sabemos esto, necesitamos crear algunos métodos para implementarlo. No hablaré sobre la lógica de la implementación específica. Puedes mirar directamente el código.


 Primero, creamos una carpeta de socket y creamos tres archivos como este, para que podamos mantenerlo fácilmente

índice.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()
  }
}


latido del corazón.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)
  }
}

encuestaRollback.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)
  }
}

De esta manera, se encapsula un Socket completo, entonces, ¿cómo debemos usarlo?

Podemos crear un archivo ws.js y encapsular el método que necesitamos en una clase nuevamente.
En cuanto a algunas personas que preguntan por qué necesitamos encapsular una clase sobre la base original, porque el método expuesto debe ser breve y claro, y la clase original También se puede usar directamente.Si solo dos personas lo desarrollan, ¡no será fácil para la otra persona comenzar!

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>)

 Bien, en este punto puede crear ws directamente para usar métodos como suscripción, cancelación de suscripción, cierre de conexión ws, etc. En cuanto a las cosas detrás del Socket, no necesitamos saberlo, siempre que nuestro código esté libre de errores. .

Aquí agrego el significado del parámetro subscribe

* carga útil Contenido requerido de suscripción, por ejemplo, si queremos suscribirnos a la información del saldo de la billetera, el fondo necesita que enviemos una identificación, luego pasaremos {id: 'saldo'} y similares

* retroceder Durante la desconexión anormal de la conexión ws, los datos en la página aún deben actualizarse, por lo que usamos la función de retroceder para sondear y actualizar los datos

* devolución de llamada Como sugiere el nombre, se llamará al método correspondiente después de que ws devuelva los datos

Supongo que te gusta

Origin blog.csdn.net/weixin_42335036/article/details/118214773
Recomendado
Clasificación