js réalise l'encapsulation de la connexion WebSocket

Tout d'abord, si le projet doit interagir avec le client et le serveur pendant une longue période et sans temps et sans interruption, l'utilisation de la connexion WebSocket est le meilleur choix, comme le projet d'échange

Alors, quelles fonctions sont nécessaires pour créer un WebSocket, et comment peut-il être un système ws robuste ?

Nous avons d'abord besoin de plusieurs fonctions de base :

1. onopen (rappel réussi de la connexion ws)

2. onmessage (ws renvoie un rappel de données)

3. onclose (ws fermer le rappel)

4. onerror (rappel d'erreur ws)

 Lorsque notre ws a ces méthodes, nous devons ajouter quelques méthodes pour interagir avec le serveur

1. abonnez-vous

2. se désabonner (se désabonner)

3. requête (envoi des données au serveur)

 Avec ces méthodes, nous devons considérer certains points anormaux

1. reconnecter (reconnexion ws, lorsque ws est anormal et déconnecté, reconnecter ws)

2. reSubscribe (réabonnez-vous, lorsque ws se reconnecte, réabonnez-vous aux événements précédemment souscrits)

3. Heartbeat (ws heartbeat, nous devons toujours vérifier si le serveur est toujours en vie)

4. pollingRollback (rappel de sauvegarde d'interrogation, lorsque ws est déconnecté, les données doivent toujours être mises à jour en permanence)

 Maintenant que nous savons cela, nous devons créer des méthodes pour l'implémenter. Je ne parlerai pas de la logique de l'implémentation spécifique. Vous pouvez directement regarder le code.


 Tout d'abord, nous créons un dossier socket et créons trois fichiers comme celui-ci, afin que nous puissions le maintenir facilement

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


battement de coeur.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)
  }
}

De cette façon, un Socket complet est encapsulé, alors comment devrions-nous l'utiliser ?

Nous pouvons créer un fichier ws.js et encapsuler à nouveau la méthode dont nous avons besoin dans une classe.Comme
pour certaines personnes demandant pourquoi nous devons encapsuler une classe sur la base d'origine, car la méthode exposée doit être brève et claire, et la classe d'origine peut également être utilisé directement.Si seulement deux personnes le développent, il ne sera pas facile pour l'autre personne de se lancer !

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, à ce stade, vous pouvez directement créer ws pour utiliser des méthodes telles que l'abonnement, le désabonnement, la fermeture de la connexion ws, etc. En ce qui concerne les choses derrière le Socket, nous n'avons pas besoin de savoir, tant que notre code est sans bogue .

Ici, j'ajoute la signification du paramètre subscribe

* Contenu requis par l'abonnement à la charge utile, par exemple, si nous voulons nous abonner aux informations sur le solde du portefeuille, l'arrière-plan a besoin de nous pour envoyer l'identifiant, puis nous passerons { id: 'balance' } et similaires

* restauration Lors de la déconnexion anormale de la connexion ws, les données de la page doivent encore être actualisées, nous utilisons donc la fonction de restauration pour interroger et mettre à jour les données

* callback Comme son nom l'indique, la méthode correspondante sera appelée après que ws aura renvoyé les données

Je suppose que tu aimes

Origine blog.csdn.net/weixin_42335036/article/details/118214773
conseillé
Classement