简易web socket封装

2,352 阅读4分钟

游手好闲,无所事事,危机四伏...轮子虽然多,不是自己的用起来总感觉怪怪的;自己造的,即使七歪八拐,也挺‘快乐’的(^o^)。

socket功能点

大部分我们使用socket的主要功能就是消息实时同步及心跳机制,自己弄之前网上找了一下,发现有挺多好用的socket封装库了,例如vue的vue-socket.io等。

socket 简易封装

  • socket具有各种状态(CLOSED,CLOSING,OPEN,CONNECTING)
  • 网站协议不同,socket的协议也不同(http://->ws://,https://->wss://)
  • 设置socket重连机制(基础重连时间及重连时间策略)
  • 设置socket心跳时间(保活机制)
  • 设置socket各种回调事件(onopen,onerror,onmessage,onclose,onreconnect)
  • 消息数据适配

兼容性及协议判断

websocket在IE10以下不支持,既然能用上socket其实我们已经不在乎这个了-.-,但是为了防止意外出错,做个简单的判断还是挺ok的:

/**
   * 判断是否支持websocket
   */
  static isSupport() {
    return /\{\s*\[native code\]\s*\}/.test(WebSocket.toString())
  }

这里默认根据当前的网站协议判断ws的协议 window.location.protocol.toLowerCase() === 'https:',然后根据协议及参数进行url的拼装:

/**
   * 获取websocket完整链接
   * @param {string} url 基础websocket URI
   */
  static getUrl(url, params = {}, isHttps = false) {
    let _path = [url]
    let _prefix = 'ws://'
    if (isHttps) {
      _prefix = 'wss://'
    }
    _path.unshift(_prefix)
    let _query = []
    for (let key in params) {
      _query.push(`${key}=${params[key]}`)
    }
    if (url.indexOf('?') > -1) {
      _path.push('&')
    } else {
      _path.push('?')
    }
    _path.push(_query.join('&'))
    return _path.join('')
  }

配置参数

根据上面功能点,初略的得出socket的基本参数配置:

<!--默认策略-->
const reconnectStrategy = (count, base) => {
  return base * count
}
static defaultConfig = {
    // 是否时https连接(默认判断当前URL协议)
    isHttps: window.location.protocol.toLowerCase() === 'https:',
    // 重连时间step(为0表示不重连)
    reconnectTime: 6 * 1000,
    // 重连时间策略
    reconnectStrategy,
    // 心跳时间间隔
    heartBeatTime: 60 * 1000,
    // 服务器数据适配
    serveAdapt: loop,
    // onopen回调
    onopen: loop,
    // onerror回调
    onerror: loop,
    // onmessage回调
    onmessage: loop,
    // onclose回调
    onclose: loop,
    // onreconnect回调
    onreconnect: loop,
  }

执行(使用)

使用类的写法,这里直接实例化类就可以返回当前ws的实例对象; loopCount表示此次重连的次数,重连成功后重置,eventPoll为简单的事件池用于注册一些简易的事件回调。

constructor(url, params = {}, config = {}) {
    // 事件注册池
    this.eventPoll = {}
    // websocket初始化参数
    this.params = params
    // 配置项
    this.config = { ...WS.defaultConfig, ...config }
    this.url = WS.getUrl(url, this.params, this.config.isHttps)
    this.loopCount = 0
    this._init()
    __log(
      '%c--> WEB SOCKET <--',
      `text-shadow: 0 1px 0 #ccc,0 2px 0 #c9c9c9,
      0 3px 0 #bbb,0 4px 0 #b9b9b9,
      0 5px 0 #aaa,0 6px 1px rgba(0,0,0,.1),
      0 0 5px rgba(0,0,0,.1),0 1px 3px rgba(0,0,0,.3),
      0 3px 5px rgba(0,0,0,.2),0 5px 10px rgba(0,0,0,.25),
      0 10px 10px rgba(0,0,0,.2),0 20px 20px rgba(0,0,0,.15);
      font-size:5em;color: transparent;
      `
    )
  }

实例化后,就会执行ws的_init方法,进行ws的创建及回调的监听,ws链接成功后通知用户并开启保活_onHeartBeat,在保活阶段有个小坑(要实时判断当前ws是否属于OPEN状态,否则手动触发_onError事件。):

/**
   * 初始化(注册回调)
   */
  async _init() {
    if (WS.isSupport()) {
      this.ws = new WebSocket(this.url)
      this.ws.onopen = this._onOpen.bind(this)
      this.ws.onmessage = this._onMessage.bind(this)
      this.ws.onclose = this._onClose.bind(this)
      this.ws.onerror = this._onError.bind(this)
    } else {
      log('Your Browser is not support WebSocket.')
    }
  }
  /**
   * 心跳函数,状态不为OPEN时触发重连机制(必须在ws open后调用该函数)
   */
  async _onHeartBeat() {
    await this._sleep(this.config.heartBeatTime)
    if (this.ws.readyState === STATE.OPEN) {
      log('>>>>ping>>>>')
      this.ws.send('ping')
      this._onHeartBeat()
    } else {
      this._onError(new Event('ws_close'))
    }
  }
  /**
   * ws开启回调
   */
  _onOpen() {
    log('websocket已打开,开启心跳')
    this.loopCount = 0
    this.config.onopen(this.ws)
    this._onHeartBeat()
  }
  /**
   * 消息监听回调
   * @param {Object} msg 服务器推送消息
   */
  _onMessage(msg) {
    try {
      let _data = JSON.parse(msg.data)
      this.config.onmessage(_data)
      let _events = []
      // code码事件
      let _code = Number(_data.code)
      let _codeEvents = this.eventPoll[_code]
      if (_codeEvents) {
        _events.push(..._codeEvents)
      }
      for (let _cb of _events) {
        _cb(_data)
      }
    } catch (error) {
      log('数据解析错误', error)
    }
  }
  /**
   * ws关闭回调
   */
  _onClose(e) {
    log(`>>>>Socket已关闭>>>>`)
    this.config.onclose(e)
  }
  /**
   * ws出错回调
   */
  _onError(e) {
    this.config.onerror(e)
    if (!this.config.reconnectTime) {
      return
    }
    let _delayTime = this.config.reconnectStrategy(this.loopCount++, this.config.reconnectTime)
    log(`>>>>${_delayTime / 1000}s后尝试重新连接>>>>`)
    this.config.onreconnect(_delayTime, this.loopCount)
    this._sleep(_delayTime).then(() => {
      log(`>>>>第${this.loopCount}次重新连接>>>>`)
      this._init()
    })
  }
  • _onError回调中,判断reconnectTime是否大于0(0表示不会重连),然后根据当前轮次及重连基础时间得出当前过多久开始重连_delayTime(重连就是简单的重新初始化ws-即重新调用_init) 以下是eventPoll的简易实现(类型及回调):
   /**
   * 订阅消息回调
   * @param {Any} type 事件码
   * @param {Function} cb 事件回调
   * @returns {Function} 取消订阅事件
   */
  subscribe(type, cb) {
    let _poll = this.eventPoll[type] || (this.eventPoll[type] = [])
    let _index = _poll.indexOf(cb)
    if (_index === -1) {
      _poll.push(cb)
      _index = _poll.length - 1
    }
    return () => {
        let _i=_poll.findIndex(p=>p===cb)
        if(_i>-1){
            _poll.splice(_i, 1)
            return true
        }
        return false
    }
  }
  • 最后在退出当前页面时要销毁当前ws(这里设置reconnectTime为0是为了关闭_onError中不断重连的机制判断):
   /**
   * 关闭重连,销毁
   */
  closeReconnect() {
    this.config.reconnectTime = 0
    this.ws.close()
  }

安装使用

npm使用:

npm i '@qiangkun/sdk'

import { Easysocket } from '@qiangkun/sdk'

const EVENT_MAP = {
  201: 'sw_eva_pack_update',
  202: 'sw_eva_video_update',
  203: 'sw_eva_nego_update',
  204: 'sw_eva_wait_update',
  '5563': 'token_expire',
  '3002': 'login_other'
}

let sw = new Easysocket(
  'xxx',
  {
    app_id: 'app_agency',
    access_token: getToken()
  },
  {
    onerror: (e) => {
      console.log('error',e)
    },
    onclose: () => {
      console.log('close')
    }
  }
)
for (let code in EVENT_MAP) {
  sw.subscribe(code, msg => {
  <!--使用vue事件机制-->
    bus.$emit(EVENT_MAP[code], msg)
  })
}

结语

自己倒腾,锅自己认,自己背--

不间断的拨弄原生js其实也算是对自己能力提升有所帮助,拨弄不了后面反正还有一堆的库使用,这样边学习边积累还是不错的~_~。