js realisiert die Kapselung der WebSocket-Verbindung

Wenn das Projekt über einen längeren Zeitraum und ohne Zeit und Unterbrechung mit dem Client und dem Server interagieren muss, ist die Verwendung einer WebSocket-Verbindung die beste Wahl, z. B. ein Austauschprojekt

Welche Funktionen sind also erforderlich, um einen WebSocket zu erstellen, und wie kann daraus ein robustes WS-System werden?

Zunächst benötigen wir einige Grundfunktionen:

1. onopen (WS-Verbindung erfolgreicher Rückruf)

2. onmessage (ws gibt Datenrückruf zurück)

3. onclose (WS-Close-Rückruf)

4. onerror (WS-Fehlerrückruf)

 Wenn unser WS über diese Methoden verfügt, müssen wir einige Methoden hinzufügen, um mit dem Server zu interagieren

1. Abonnieren

2. unSubscibe (abmelden)

3. Anfrage (Daten an den Server senden)

 Bei diesen Methoden müssen wir einige ungewöhnliche Punkte berücksichtigen

1. reConnect (WS-Wiederverbindung, wenn WS abnormal ist und die Verbindung getrennt ist, WS erneut verbinden)

2. reSubscribe (erneut abonnieren, wenn ws wieder eine Verbindung herstellt, abonnieren Sie die zuvor abonnierten Ereignisse erneut)

3. Heartbeat (WS-Heartbeat, wir müssen immer prüfen, ob der Server noch aktiv ist)

4. pollingRollback (Abfrage-Backup-Rückruf, wenn ws getrennt wird, müssen die Daten weiterhin kontinuierlich aktualisiert werden)

 Da wir das nun wissen, müssen wir einige Methoden erstellen, um es zu implementieren. Ich werde nicht auf die Logik der spezifischen Implementierung eingehen. Sie können sich den Code direkt ansehen.


 Zuerst erstellen wir einen Socket-Ordner und drei Dateien wie diese, damit wir ihn einfach verwalten können

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

Auf diese Weise wird ein vollständiger Socket gekapselt. Wie sollten wir ihn also verwenden?

Wir können eine ws.js-Datei erstellen und die Methode, die wir benötigen, erneut in einer Klasse kapseln.
Einige Leute fragen, warum wir eine Klasse auf der ursprünglichen Basis kapseln müssen, da die offengelegte Methode kurz und klar sein muss und die ursprüngliche Klasse dies auch kann direkt verwendet werden. Wenn nur zwei Personen es entwickeln, wird es für die andere Person nicht einfach sein, damit anzufangen!

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

 Okay, an dieser Stelle können Sie WS direkt erstellen, um Methoden wie Abonnement, Abmeldung, Schließen der WS-Verbindung usw. zu verwenden. Was die Dinge hinter dem Socket betrifft, müssen wir nichts wissen, solange unser Code fehlerfrei ist .

Hier füge ich die Bedeutung des Subscribe-Parameters hinzu

* Payload-Abonnement erforderlicher Inhalt. Wenn wir beispielsweise Informationen zum Wallet-Kontostand abonnieren möchten, müssen wir im Hintergrund eine ID senden, dann übergeben wir { id: 'balance' } und dergleichen

* Rollback Während der abnormalen Trennung der WS-Verbindung müssen die Daten auf der Seite noch aktualisiert werden, daher verwenden wir die Rollback-Funktion, um die Daten abzufragen und zu aktualisieren

* Rückruf Wie der Name schon sagt, wird die entsprechende Methode aufgerufen, nachdem ws die Daten zurückgegeben hat

Ich denke du magst

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