微信基础库1.7.0之后的版本提供了新版的WebSocket API,考虑到兼容性问题,尝试整合新旧两种版本的API,进行简单的封装。

从以下几个角度对微信小程序中所提供的WebSocket API封装

  • API兼容性
  • 重连机制

小程序WebSocket API

旧版WebSocket API的使用

  • 创建 - wx.connectSocket
  • 发送消息 - sendSocketMessage
  • 监听事件 - onSocketOpen
let socketOpen = false
const socketMsgQueue = []
// 连接socket
wx.connectSocket({
  url: 'test.php'
})
// 监听打开事件
wx.onSocketOpen(function(res) {
  socketOpen = true
  for (let i = 0; i < socketMsgQueue.length; i++){
    sendSocketMessage(socketMsgQueue[i])
  }
  socketMsgQueue = []
})
// 发送消息
function sendSocketMessage(msg) {
  if (socketOpen) {
    wx.sendSocketMessage({
      data:msg
    })
  } else {
    socketMsgQueue.push(msg)
  }
}

新版WebSocket API的使用

支持1.7.0+,连接后会返回一个SocketTask对象,在该对象上监听该连接的各种事件与执行发送消息等操作。

let socketTask = wx.connectSocket({
  url: 'test.php'
})
// 监听打开事件
socketTask.onOpen(function(res) {
  socketOpen = true
  // 发送信息
  socketTask.send({msg: "hello world"})
})

并发数

  • 1.7.0 及以上版本,最多可以同时存在 5WebSocket 连接。
  • 1.7.0 以下版本,一个小程序同时只能有一个 WebSocket 连接,如果当前已存在一个 WebSocket 连接,会自动关闭该连接,并重新创建一个 WebSocket 连接。

模块封装

简单封装封装一个兼容新旧socketAPI的模块,仅考虑存在单个socket连接的情况

创建与事件监听

  • 新版: 在socket连接时会返回一个socketTask对象,监听事件是在该对象的基础上进行
  • 旧版: 直接使用wx放进行创建与监听方法

统一创建与添加监听函数

init() {
  let st = this.connect()
  this.listen(st)
  this.manualClose = false
}

创建连接

connect() {
  let st = wx.connectSocket(this.config.server)
  return st
}

添加事件监听函数

listen(st) {
  if (st !== undefined) {
    this.ws = st
    this.ws.onOpen(() => this.openHandle())
    this.ws.onError(() => this.errorHandle())
    this.ws.onClose(() => this.closeHandle())
    this.ws.onMessage(res => this.messageHandle(res))
  } else {
    wx.onSocketOpen(() => this.openHandle())
    wx.onSocketError(() => this.errorHandle())
    wx.onSocketClose(() => this.closeHandle())
    wx.onSocketMessage(res => this.messageHandle(res))
  }
}

重连机制

预设标记位

retryLock = false; // 避免多次同时重连
socketOpen = false; // 连接状态
manualClose = false; // 主动断开标记

在连接关闭监听函数中执行重连

closeHandle() {
  console.info('WebSocket closed')
  this.socketOpen = false
  this.retryLock = false
  // 不论是error还是close都会触发close事件,统一在这里进行重连
  // 初次连接失败不进行重连(失败不会进入到onOpen的监听事件中,那时未声明retryTimes变量)
  this.retryTimes !== undefined && this.reconnect()
}

判断重连锁与是否主动断开进行重连

reconnect() {
  if (this.retryLock) return
  this.retryLock = true
  // 若manualClose为true,表明不是主动断开
  if (!this.manualClose) {
    // 开始重连
    setTimeout(() => {
      this.retry()
    }, this.retryInterval)
  }
}

重连函数,包含重连次数的限制

retry() {
  if (
    this.socketOpen ||
    (this.retryTimes > 0 && this.retryCount <= this.retryTimes)
  ) {
    console.warn(`reconnect ending. reconnect ${this.retryTimes} times`)
    if (!this.socketOpen) {
      this.config.closeCallback()
    }
    return
  }
  this.retryTimes += 1
  console.warn(`[ ${this.retryTimes} ]th reconnect WebSocket...`)
  this.init()
}

消息队列

添加消息队列,当重连后自动发送缓存消息

openHandle() {
  this.retryTimes = 0
  this.socketOpen = true
  this.retryLock = false

  this.messageQueue.map(e => this.send(e))
  this.messageQueue = []
}

若发送时断开则先将消息缓存到消息队列中

send(value) {
  let data = this.msgWrapper(value)
  data = JSON.stringify(data)
  if (!this.socketOpen) {
    this.messageQueue.push(data)
  } else {
    if (this.ws) {
      this.ws.send({ data })
    } else {
      wx.sendSocketMessage({ data })
    }
  }
}

辅助函数

添加一些包裹消息格式的工具函数

messageIndex = 0;
helper = {
  isPlainObject: val =>
    Object.prototype.toString.call(val) === '[object Object]',
  nextId: () => {
    this.messageIndex += 1
    return this.messageIndex
  },
  id: () => Date.now() + '.' + this.helper.nextId()
};
msgWrapper(data) {
  let msg = data
  if (this.helper.isPlainObject(msg)) {
    if (msg.type) {
      return msg
    } else {
      return this.msgWrapper({ type: 'message', msg, id: this.helper.id() })
    }
  } else {
    return this.msgWrapper({ type: 'message', msg, id: this.helper.id() })
  }
}

完整代码

export default class WXWebSocket {
  messageQueue = []; // 消息队列
  retryLock = false; // 避免多次同时重连
  socketOpen = false;
  manualClose = false; // 主动断开标记
  constructor(config) {
    this.config = config || {}
    // 重连间隔
    this.retryInterval =
      this.config.retryInterval && this.config.retryInterval > 100
        ? this.config.retryInterval
        : 3000
    // 重连次数
    this.retryCount = this.config.retryCount || 5
    this.init()
  }
  init() {
    let st = this.connect()
    this.listen(st)
    this.manualClose = false
  }
  connect() {
    let st = wx.connectSocket(this.config.server)
    console.log('current socket: ', st)
    return st
  }
  listen(st) {
    // 添加监听事件
    if (st !== undefined) {
      // 若存在SocketTask,则要通过readyState判断状态
      // CONNECTING: 0
      // OPEN: 1
      // CLOSING: 2
      // CLOSE: 3
      this.ws = st
      this.ws.onOpen(() => this.openHandle())
      this.ws.onError(() => this.errorHandle())
      this.ws.onClose(() => this.closeHandle())
      this.ws.onMessage(res => this.messageHandle(res))
    } else {
      wx.onSocketOpen(() => this.openHandle())
      wx.onSocketError(() => this.errorHandle())
      wx.onSocketClose(() => this.closeHandle())
      wx.onSocketMessage(res => this.messageHandle(res))
    }
  }
  close() {
    this.manualClose = true
    if (this.ws) {
      this.ws.close()
    } else {
      wx.closeSocket()
    }
  }
  send(value) {
    console.log('send value: ', value)
    let data = this.msgWrapper(value)
    data = JSON.stringify(data)
    if (!this.socketOpen) {
      // add new message to queue
      this.messageQueue.push(data)
    } else {
      if (this.ws) {
        this.ws.send({ data })
      } else {
        wx.sendSocketMessage({ data })
      }
    }
  }
  openHandle() {
    console.info('WebSocket connected')
    this.retryTimes = 0
    this.socketOpen = true
    this.retryLock = false

    this.messageQueue.map(e => this.send(e))
    this.messageQueue = []
  }
  errorHandle() {
    console.error('WebSocket error')
    this.socketOpen = false
  }
  closeHandle() {
    console.info('WebSocket closed')
    this.socketOpen = false
    this.retryLock = false
    // 不论是error还是close都会触发close事件,统一在这里进行重连
    // 初次连接失败不进行重连(失败不会进入到onOpen的监听事件中,那时未声明retryTimes变量)
    this.retryTimes !== undefined && this.reconnect()
  }
  reconnect() {
    if (this.retryLock) return
    this.retryLock = true
    // 若manualClose为true,表明不是主动断开
    if (!this.manualClose) {
      // 开始重连
      setTimeout(() => {
        this.retry()
      }, this.retryInterval)
    }
  }
  retry() {
    if (
      this.socketOpen ||
      (this.retryTimes > 0 && this.retryCount <= this.retryTimes)
    ) {
      console.warn(`end reconnect. reconnect ${this.retryTimes} times`)
      if (!this.socketOpen) {
        this.config.closeCallback()
      }
      return
    }
    this.retryTimes += 1
    console.warn(`[ ${this.retryTimes} ]th reconnect WebSocket...`)
    this.init()
  }
  messageHandle(res) {
    this.config.responseCallback(res)
  }
  msgWrapper(data) {
    let msg = data
    if (this.helper.isPlainObject(msg)) {
      if (msg.type) {
        return msg
      } else {
        return this.msgWrapper({ type: 'message', msg, id: this.helper.id() })
      }
    } else {
      return this.msgWrapper({ type: 'message', msg, id: this.helper.id() })
    }
  }
  messageIndex = 0;
  helper = {
    isPlainObject: val =>
      Object.prototype.toString.call(val) === '[object Object]',
    nextId: () => {
      this.messageIndex += 1
      return this.messageIndex
    },
    id: () => Date.now() + '.' + this.helper.nextId()
  };
}

使用

创建连接

let socketTask = new WXWebSocket({
  server: this.wsServerOption,
  responseCallback: e => {
    let { data } = e
    let { msg } = JSON.parse(data)
    this.msgStack.push(msg)
  },
  closeCallback: () => {
    this.socketTask = null
  }
})
this.socketTask = socketTask

发送消息

sendWSMessage(msg) {
  this.msgStack.push(msg)
  this.socketTask && this.socketTask.send(msg)
},

关闭连接

closeWS() {
  if (!this.socketTask) return
  if (this.socketTask.socketOpen) {
    this.socketTask.close()
    this.socketTask = null
  }
}