前言
前几天我用Vue3重构了我那个Vue2的开源项目,最后还遗留了一个问题:项目中用的一个websocket插件还不能正常使用。于是,我决定重写这个插,让其支持Vue3。
本文将记录下重写这个插件的过程并将其发布至npm仓库,顺便给插件作者提个PR,欢迎各位感兴趣的开发者阅读本文。
插件解读
image-20201103005333494
如上图所示就是即将要重构的插件,目前有735个star,我们先将插件代码clone
到本地。
git clone https://github.com/nathantsoi/vue-native-websocket
下载到本地后,用你喜欢的ide
打开它,其目录如下:
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] }) } } } }) } }