使用Vue3+TS重构百星websocket插件(上)

简介: 使用Vue3+TS重构百星websocket插件(上)

前言


前几天我用Vue3重构了我那个Vue2的开源项目,最后还遗留了一个问题:项目中用的一个websocket插件还不能正常使用。于是,我决定重写这个插,让其支持Vue3。

本文将记录下重写这个插件的过程并将其发布至npm仓库,顺便给插件作者提个PR,欢迎各位感兴趣的开发者阅读本文。


插件解读


640.png

                   image-20201103005333494


如上图所示就是即将要重构的插件,目前有735个star,我们先将插件代码clone到本地。


git clone https://github.com/nathantsoi/vue-native-websocket


下载到本地后,用你喜欢的ide打开它,其目录如下:


640.png

                       image-20201101194150523


目录解读


经过一番梳理后,其各个目录的作用如下:


  • vue-native-websocket 项目文件夹
  • Emitter.js websocket的事件队列与分发的实现
  • Main.js vue 插件入口代码
  • Observer.js 观察者模式,websocket服务核心功能封装
  • build.js 编译后的代码文件
  • dist 编译后的项目文件夹
  • node_modules 项目依赖库
  • src 项目源码文件夹
  • test 单元测试文件
  • .eslintrc.json 项目的eslint配置
  • .gitignore 上传至git仓库需要忽略的文件
  • .nvmrc 指定项目期望用的node版本
  • .travis.yml 自动化构建配置文件
  • CHANGELOG.md 版本发布记录文件
  • npm-shrinkwrap.json npm包版本锁定文件
  • package.json 项目依赖配置文件
  • PUBLISH.md 修改完插件后的发布规范
  • README.md 插件使用文档
  • webpack.config.js webpack配置文件
  • yarn.lock yarn包版本锁定文件


读完代码后,我们发现他的实现逻辑很精简,一个字:妙。


该插件的核心代码就src目录下的3个文件,接下来我们就从插件的入口文件Main.js开始解读。


如下所示,它引入了两个文件以及Vue官方要求的插件作为一个对象时必须提供的install方法。


import Observer from './Observer'
import Emitter from './Emitter'
export default {
    install (Vue, connection, opts = {}) {
      // ... 其它代码省略 ... //
    }
}

那么,我们就先来看看第一个引入的文件Observer.js的代码。

如下所示,它引入了Emitter.js文件,以及它自身的实现代码。

import Emitter from './Emitter'
export default class {
  constructor (connectionUrl, opts = {}) {
    // ... 其它代码省略... //
  })
}


Emitter.js


同样的,我们先从他引入的文件开始读,即Emitter.js,其代码如下,我读完代码后并添加了相关注释,它实现了一个事件监听队列,以及一个事件触发函数emit


class Emitter {
  constructor () {
    this.listeners = new Map()
  }
    /**
     * 添加事件监听
     * @param label 事件名称
     * @param callback 回调函数
     * @param vm this对象
     * @return {boolean}
    */
  addListener (label, callback, vm) {
    if (typeof callback === 'function') {
      // label不存在就添加
      this.listeners.has(label) || this.listeners.set(label, [])
      // 向label添加回调函数
      this.listeners.get(label).push({callback: callback, vm: vm})
      return true
    }
    return false
  }
    /**
     * 移除监听
     * @param label 事件名称
     * @param callback 回调函数
     * @param vm this对象
     * @return {boolean}
    */
  removeListener (label, callback, vm) {
    // 从监听列表中获取当前事件
    let listeners = this.listeners.get(label)
    let index
    if (listeners && listeners.length) {
      // 寻找当前事件在事件监听列表的位置
      index = listeners.reduce((i, listener, index) => {
        if (typeof listener.callback === 'function' && listener.callback === callback && listener.vm === vm) {
          i = index
        }
        return i
      }, -1)
      if (index > -1) {
        // 移除事件
        listeners.splice(index, 1)
        this.listeners.set(label, listeners)
        return true
      }
    }
    return false
  }
  /**
     * 触发监听
     * @param label 事件名称
     * @param args 参数
     * @return {boolean}
  */
  emit (label, ...args) {
     // 获取事件列表中存储的事件
    let listeners = this.listeners.get(label)
    if (listeners && listeners.length) {
      listeners.forEach((listener) => {
        // 扩展callback函数,让其拥有listener.vm中的方法
        listener.callback.call(listener.vm, ...args)
      })
      return true
    }
    return false
  }
}
export default new Emitter()


Observer.js


接下来,我们在回过头来看Observer.js的代码,他实现了websocket服务核心功能的封装,是这个插件的核心。它的constructor部分代码如下所示,他定义了插件调用者可以传的参数以及初始值。


constructor (connectionUrl, opts = {}) {
    // 获取参数中的format并将其转成小写
    this.format = opts.format && opts.format.toLowerCase()
   // 如果url以//开始对其进行处理添加正确的websocket协议前缀
    if (connectionUrl.startsWith('//')) {
      // 当前网站如果为https请求则添加wss前缀否则添加ws前缀
      const scheme = window.location.protocol === 'https:' ? 'wss' : 'ws'
      connectionUrl = `${scheme}:${connectionUrl}`
    }
    // 将处理好的url和opts赋值给当前类内部变量
    this.connectionUrl = connectionUrl
    this.opts = opts
    // 是否开启重连,默认值为false
    this.reconnection = this.opts.reconnection || false
   // 最大重连次数,默认值为无穷大
    this.reconnectionAttempts = this.opts.reconnectionAttempts || Infinity
    // 重连间隔时间,默认为1s
    this.reconnectionDelay = this.opts.reconnectionDelay || 1000
    // 重连超时id,默认为0
    this.reconnectTimeoutId = 0
   // 已重连次数,默认为0
    this.reconnectionCount = 0
   // 传输数据时的处理函数
    this.passToStoreHandler = this.opts.passToStoreHandler || false
   // 建立连接
    this.connect(connectionUrl, opts)
    // 如果配置参数中有传store就将store赋值
    if (opts.store) { this.store = opts.store }
    // 如果配置参数中有传vuex的同步处理函数就将mutations赋值
    if (opts.mutations) { this.mutations = opts.mutations }
    // 事件触发
    this.onEvent()
}


连接函数


我们再来看看connet方法的实现,它的代码如下,它会根据用户传入的websocket服务端地址以及插件参数来建立websocket连接。


// 连接websocket
  connect (connectionUrl, opts = {}) {
    // 获取配置参数传入的协议
    let protocol = opts.protocol || ''
    // 如果没传协议就建立一个正常的websocket连接否则就创建带协议的websocket连接
    this.WebSocket = opts.WebSocket || (protocol === '' ? new WebSocket(connectionUrl) : new WebSocket(connectionUrl, protocol))
    // 启用json发送
    if (this.format === 'json') {
      // 如果websocket中没有senObj就添加这个方法对象
      if (!('sendObj' in this.WebSocket)) {
        // 将发送的消息转为json字符串
        this.WebSocket.sendObj = (obj) => this.WebSocket.send(JSON.stringify(obj))
      }
    }
    return this.WebSocket
  }


重连函数


我们再来看看reconnect方法的实现,它的代码如下,它会读取用户传进来的最大重连次数,然后重新与websocket服务端建立链接。


// 重新连接
 reconnect () {
    // 已重连次数小于等于设置的连接次数时执行重连
    if (this.reconnectionCount <= this.reconnectionAttempts) {
      this.reconnectionCount++
      // 清理上一次重连时的定时器
      clearTimeout(this.reconnectTimeoutId)
      // 开始重连
      this.reconnectTimeoutId = setTimeout(() => {
        // 如果启用vuex就触发vuex中的重连方法
        if (this.store) { this.passToStore('SOCKET_RECONNECT', this.reconnectionCount) }
        // 重新连接
        this.connect(this.connectionUrl, this.opts)
        // 触发WebSocket事件
        this.onEvent()
      }, this.reconnectionDelay)
    } else {
      if (this.store) {    
        // 如果启用vuex则触发重连失败方法
        this.passToStore('SOCKET_RECONNECT_ERROR', true) }
    }
  }


事件触发函数


我们再来看看onEvent函数,它的实现代码如下,它会调用Emitter中的emit方法,对websocket中的4个监听事件进行分发扩展,交由Emitter类来管理。


// 事件分发  
 onEvent () {
    ['onmessage', 'onclose', 'onerror', 'onopen'].forEach((eventType) => {
      this.WebSocket[eventType] = (event) => {
        Emitter.emit(eventType, event)
         // 调用vuex中对应的方法
        if (this.store) { this.passToStore('SOCKET_' + eventType, event) }
        // 处于重新连接状态切事件为onopen时执行
        if (this.reconnection && eventType === 'onopen') {
           // 设置实例
          this.opts.$setInstance(event.currentTarget)
          // 清空重连次数
          this.reconnectionCount = 0
        }
        // 如果处于重连状态且事件为onclose时调用重连方法
        if (this.reconnection && eventType === 'onclose') { this.reconnect() }
      }
    })
  }


vuex事件处理函数


我们再来看看处理vuex事件的实现函数,它的实现代码如下,它用于触发vuex中的方法,它允许调用者传passToStoreHandler事件处理函数,用于触发前的事件处理。


/**
     * 触发vuex中的方法
     * @param eventName 事件名称
     * @param event 事件
  */
 passToStore (eventName, event) {
    // 如果参数中有传事件处理函数则执行自定义的事件处理函数,否则执行默认的处理函数
    if (this.passToStoreHandler) {
      this.passToStoreHandler(eventName, event, this.defaultPassToStore.bind(this))
    } else {
      this.defaultPassToStore(eventName, event)
    }
  }
 /**
     * 默认的事件处理函数
     * @param eventName 事件名称
     * @param event 事件
  */
  defaultPassToStore (eventName, event) {
    // 事件名称开头不是SOCKET_则终止函数
    if (!eventName.startsWith('SOCKET_')) { return }
    let method = 'commit'
    // 事件名称字母转大写
    let target = eventName.toUpperCase()
    // 消息内容
    let msg = event
    // data存在且数据为json格式
    if (this.format === 'json' && event.data) {
      // 将data从json字符串转为json对象
      msg = JSON.parse(event.data)
      // 判断msg是同步还是异步
      if (msg.mutation) {
        target = [msg.namespace || '', msg.mutation].filter((e) => !!e).join('/')
      } else if (msg.action) {
        method = 'dispatch'
        target = [msg.namespace || '', msg.action].filter((e) => !!e).join('/')
      }
    }
    if (this.mutations) {
      target = this.mutations[target] || target
    }
    // 触发store中的方法
    this.store[method](target, msg)
  }


Main.js


上面我们读完了插件的核心实现代码,最后我们来看看插件的入口文件,它的代码如下,他会将我们前面实现的websocket相关封装应用到Vue全局。他做了以下事情:


  • 全局挂载$socket属性,便于访问socket建立的socket连接
  • 启用手动连接时,向全局挂载手动连接方法和关闭连接方法
  • 全局混入,添加socket事件监听,组件销毁前移除全局添加的方法


import Observer from './Observer'
import Emitter from './Emitter'
export default {
  install (Vue, connection, opts = {}) {
    // 没有传入连接,抛出异常
    if (!connection) { throw new Error('[vue-native-socket] cannot locate connection') }
    let observer = null
    opts.$setInstance = (wsInstance) => {
      // 全局属性添加$socket
      Vue.prototype.$socket = wsInstance
    }
    // 配置选项中启用手动连接
    if (opts.connectManually) {
      Vue.prototype.$connect = (connectionUrl = connection, connectionOpts = opts) => {
        // 调用者传入的参数中添加set实例
        connectionOpts.$setInstance = opts.$setInstance
        // 创建Observer建立websocket连接
        observer = new Observer(connectionUrl, connectionOpts)
        // 全局添加$socket
        Vue.prototype.$socket = observer.WebSocket
      }
      // 全局添加连接断开处理函数
      Vue.prototype.$disconnect = () => {
        if (observer && observer.reconnection) {
          // 重新连接状态改为false
          observer.reconnection = false
        }
        // 如果全局属性socket存在则从全局属性移除
        if (Vue.prototype.$socket) {
          // 关闭连接
          Vue.prototype.$socket.close()
          delete Vue.prototype.$socket
        }
      }
    } else {
      // 未启用手动连接
      observer = new Observer(connection, opts)
      // 全局添加$socket属性,连接至websocket服务器
      Vue.prototype.$socket = observer.WebSocket
    }
    const hasProxy = typeof Proxy !== 'undefined' && typeof Proxy === 'function' && /native code/.test(Proxy.toString())
    Vue.mixin({
      created () {
        let vm = this
        let sockets = this.$options['sockets']
        if (hasProxy) {
          this.$options.sockets = new Proxy({}, {
            set (target, key, value) {
              // 添加监听
              Emitter.addListener(key, value, vm)
              target[key] = value
              return true
            },
            deleteProperty (target, key) {
              // 移除监听
              Emitter.removeListener(key, vm.$options.sockets[key], vm)
              delete target.key
              return true
            }
          })
          if (sockets) {
            Object.keys(sockets).forEach((key) => {
              // 给$options中添加sockets中的key
              this.$options.sockets[key] = sockets[key]
            })
          }
        } else {
          // 将对象密封,不能再进行改变
          Object.seal(this.$options.sockets)
          // if !hasProxy need addListener
          if (sockets) {
            Object.keys(sockets).forEach(key => {
              // 添加监听
              Emitter.addListener(key, sockets[key], vm)
            })
          }
        }
      },
      beforeDestroy () {
        if (hasProxy) {
          let sockets = this.$options['sockets']
          if (sockets) {
            Object.keys(sockets).forEach((key) => {
              // 销毁前如果代理存在sockets存在则移除$options中给sockets添加过的key
              delete this.$options.sockets[key]
            })
          }
        }
      }
    })
  }
}
相关文章
|
5月前
|
小程序 Shell Linux
workman(二)thinkphp5.0安装websocket插件workerman
首先说明一下我使用的PHP框架是thinkphp5.0。 当然,workerman这个插件不是只有thinkphp5.0可以使用。 具体的安装方法,thinkphp5.0的官方手册中是给出了明确的说明 请移步《thinkphp5.0官方手册》
89 0
|
资源调度 JavaScript IDE
使用Vue3+TS重构百星websocket插件(下)
使用Vue3+TS重构百星websocket插件(下)
使用Vue3+TS重构百星websocket插件(下)
|
网络协议 前端开发 安全
websocket和http的瓜葛以及websocket协议实现
websocket和http的瓜葛以及websocket协议实现
websocket和http的瓜葛以及websocket协议实现
|
JavaScript
js实现websocket实例
js实现websocket实例
195 0
|
消息中间件 网络协议 前端开发
SpringBoot轻松整合WebSocket,实现Web在线聊天室
前面为大家讲述了 Spring Boot的整合Redis、RabbitMQ、Elasticsearch等各种框架组件;随着移动互联网的发展,服务端消息数据推送已经是一个非常重要、非常普遍的基础功能。今天就和大家聊聊在SpringBoot轻松整合WebSocket,实现Web在线聊天室,希望能对大家有所帮助。
SpringBoot轻松整合WebSocket,实现Web在线聊天室
|
网络协议 Linux 网络安全
php实现websocket实时消息推送
php实现websocket实时消息推送
429 0
php实现websocket实时消息推送
|
JavaScript 前端开发 Python
Python编程:tornado实现WebSocket通讯
以下实例有点像广播通讯,支持多个客户端连接 代码参考别人的,稍微做了一点点修改 服务器端 websocket_demo.py
98 0
Python编程:tornado实现WebSocket通讯
|
消息中间件 NoSQL 前端开发
通过WebSocket实现日志打印功能
通过WebSocket实现日志打印功能
686 0
通过WebSocket实现日志打印功能
|
网络协议 安全 Java
用Netty实现WebSocket网络聊天室
用Netty实现WebSocket网络聊天室
166 0
用Netty实现WebSocket网络聊天室