js 实现封装 WebSocket 连接

首先,项目中如果需要长时间且不定时不间断地进行客户端与服务器端交互的时候,使用 WebSocket 连接是不二之选,比如交易所项目

那么创建一个 WebSocket 需要什么功能,怎么才能是一个健壮的 ws 体系呢?

首先我们需要几个基础功能:

1. onopen(ws 连接成功回调)

2. onmessage (ws 返回数据回调)

3. onclose (ws 关闭回调)

4. onerror (ws 报错回调)

 当我们的 ws 有这些方法之后,我们就需要加几个方法用来和服务器端交互

1. subscibe (订阅)

2. unSubscibe (取消订阅)

3. request (向服务器发送数据)

 有了这些方法之后我们就需要考虑一些异常点

1. reConnect (ws 重连,当 ws 发生异常并断开连接之后,重新连接 ws)

2. reSubscribe (重新订阅,当 ws 重新连接之后,将之前订阅的事件重新订阅)

3. Heartbeat (ws 心跳,我们需要时刻检测服务器端是否还活着)

4. pollingRollback (轮询备用回调,当 ws 断开连接之后,数据还是需要持续更新)

 既然知道了这些,那我们需要创建一些方法来实现了,具体实现的逻辑就不讲了,大家可以直接看代码。


 首先我们创建一个 socket 文件夹,像这样创建三个文件,这样我们方便维护

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

这样一来,一个完整的 Socket 就封装完成了,那我们该如何使用呢?

我们可以创建一个 ws.js 文件,把我们需要的方法再次封装一个类
至于有人问为什么要在原有基础上再封装一个类,因为暴露的方法必须简介明了的原则吧,也可以直接使用原始类,只是两人开发的话,另外一个人不容易上手吧!

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,到这一步你就可以直接创建 ws 来使用订阅,取消订阅,关闭ws连接等的方法了,至于 Socket 背后的那些事我们并不需要知道,只要保证我们的代码没有 bug 就可以了。

这里我补充一下 subscibe 参数的含义

* payload 订阅需要的内容,比如我们要订阅钱包余额信息,后台需要我们发送 id,那我们就传 { id: 'balance' } 之类的

* rollback ws 连接发生异常断开的期间,页面的数据还是需要刷新,那么我们就是用 rollback 函数来轮询更新数据

* callback 顾名思义,ws 返回数据后会调用该对应方法

猜你喜欢

转载自blog.csdn.net/weixin_42335036/article/details/118214773
今日推荐