背景
有一個項目,今年12月份開始重構,項目涉及到了socket。但是socket用的是以前一個開發人員封裝的包(這個一直被當前的成員吐槽為什么不用已經千錘百煉的輪子)。因此,趁著這個重構的機會,將vue-socket.io引入,后端就用socket.io。我也好奇看了看vue-socket.io的源碼(我不會說是因為這個庫的文檔實在太簡略了,我為了穩點去看源碼了解該怎么用)
開始
文件架構

我們主要看src下的三個文件,可以看出該庫是用了觀察者模式
Main.js
// 這里創建一個observe對象,具體做了什么可以看Observer.js文件let observer = new Observer(connection, store)// 將socket掛載到了vue的原型上,然后就可以// 在vue實例中就可以this.$socket.emit('xxx', {})Vue.prototype.$socket = observer.Socket;import store from './yourstore'Vue.use(VueSocketio, socketio('http://socketserver.com:1923'), store);我們如果要使用這個庫的時候,一般是這樣寫的代碼(上圖2)。上圖一的connection和store就分別是圖二的后兩個參數。意思分別為socket連接的url和vuex的store啦。圖一就是將這兩個參數傳進Observer,新建了一個observe對象,然后將observe對象的socket屬性掛載在Vue原型上。那么我們在Vue的實例中就可以直接 this.$sockets.emit('xxx', {})了
// 👇就是在vue實例的生命周期做一些操作Vue.mixin({  created(){    let sockets = this.$options['sockets']    this.$options.sockets = new Proxy({}, {      set: (target, key, value) => {        Emitter.addListener(key, value, this)        target[key] = value        return true;      },      deleteProperty: (target, key) => {        Emitter.removeListener(key, this.$options.sockets[key], this)        delete target.key;        return true      }    })    if(sockets){      Object.keys(sockets).forEach((key) => {        this.$options.sockets[key] = sockets[key];      });    }  },  /**   * 在beforeDestroy的時候,將在created時監聽好的socket事件,全部取消監聽   * delete this.$option.sockets的某個屬性時,就會將取消該信號的監聽   */  beforeDestroy(){    let sockets = this.$options['sockets']    if(sockets){      Object.keys(sockets).forEach((key) => {        delete this.$options.sockets[key]      });    }  }下面就是在Vue實例的生命周期做一些操作。創建的時候,將實例中的$options.sockets的值先緩存下來,再將$options.sockets指向一個proxy對象,這個proxy對象會攔截外界對它的賦值和刪除屬性操作。這里賦值的時候,鍵就是socket事件,值就是回調函數。賦值時,就會監聽該事件,然后將回調函數,放進該socket事件對應的回調數組里。刪除時,就是取消監聽該事件了,將賦值時壓進回調數組的那個回調函數,刪除,表示,我不監聽了。這樣寫法,其實就跟vue的響應式一個道理。也因此,我們就可以動態地添加和移除監聽socket事件了,比如this.$option.sockets.xxx = () => ()和 delete this.$option.sockets.xxx。最后將緩存的值,依次賦值回去,那么如下圖的寫法就會監聽到事件并執行回調函數了:
var vm = new Vue({ sockets:{  connect: function(){   console.log('socket connected')  },  customEmit: function(val){   console.log('this method was fired by the socket server. eg: io.emit("customEmit", data)')  } }, methods: {  clickButton: function(val){    // $socket is socket.io-client instance    this.$socket.emit('emit_method', val);  } }})Emitter.js
Emitter.js主要是寫了一個Emitter對象,該對象提供了三個方法:
addListener
addListener(label, callback, vm) {  // 回調函數類型是回調函數才對  if(typeof callback == 'function'){    // 這里就很常見的寫法了,判斷map中是否已經注冊過該事件了    // 如果沒有,就初始化該事件映射的值為空數組,方便以后直接存入回調函數    // 反之,直接將回調函數放入數組即可    this.listeners.has(label) || this.listeners.set(label, []);    this.listeners.get(label).push({callback: callback, vm: vm});    return true  }  return false}其實很常規啦,實現發布訂閱者模式或者觀察者模式代碼的同學都很清楚這段代碼的意思。Emiiter用一個map來存儲事件以及它對應的回調事件數組。這段代碼先判斷map中是否之前已經存儲過了該事件,如果沒有,初始化該事件對應的值為空數組,然后將當前的回調函數,壓進去,反之,直接壓進去。
removeListener
if (listeners && listeners.length) {  index = listeners.reduce((i, listener, index) => {    return (typeof listener.callback == 'function' && listener.callback === callback && listener.vm == vm) ?      i = index :      i;  }, -1);  if (index > -1) {    listeners.splice(index, 1);    this.listeners.set(label, listeners);    return true;  }}return false;這里也很簡單啦,獲取該事件對應的回調數組。如果不為空,就去尋找需要移除的回調,找到后,直接刪除,然后將新的回調數組覆蓋原來的那個就可以了
emit
if (listeners && listeners.length) {  listeners.forEach((listener) => {    listener.callback.call(listener.vm,...args)  });  return true;}return false;這里就是監聽到事件后,執行該事件對應的回調函數,注意這里的call,因為監聽到事件后我們可能要修改下vue實例的數據或者調用一些方法,用過vue的同學都知道我們都是this.xxx來調用的,所以一定得將回調函數的this指向vue實例,這也是為什么存回調事件時也要把vue實例存下來的原因。
Observer.js
constructor(connection, store) {  // 這里很明白吧,就是判斷這個connection是什么類型  // 這里的處理就是你可以傳入一個連接好的socket實例,也可以是一個url  if(typeof connection == 'string'){    this.Socket = Socket(connection);  }else{    this.Socket = connection  }  // 如果有傳進vuex的store可以響應在store中寫的mutations和actions  // 這里只是掛載在這個oberver實例上  if(store) this.store = store;  // 監聽,啟動!  this.onEvent()}這個Observer.js里也主要是寫了一個Observer的class,以上是它的構造函數,構造函數第一件事是判斷connection是不是字符串,如果是就構建一個socket實例,如果不是,就大概是個socket的實例了,然后直接掛載在它的對象實例上。其實這里我覺得可以參數檢查嚴格點, 比如字符串被人搞怪地可能會傳入一個非法的url,對吧。這個時候判斷下,拋出一個error提醒下也好,不過應該也沒人這么無聊吧,2333。然后如果傳入了store,也掛在對象實例上吧。最后就啟動監聽事件啦。我們看看onEvent的邏輯
  onEvent(){    // 監聽服務端發來的事件,packet.data是一個數組    // 第一項是事件,第二個是服務端傳來的數據    // 然后用emit通知訂閱了該信號的回調函數執行    // 如果有傳入了vuex的store,將該事件和數據傳入passToStore,執行passToStore的邏輯    var super_onevent = this.Socket.onevent;    this.Socket.onevent = (packet) => {      super_onevent.call(this.Socket, packet);      Emitter.emit(packet.data[0], packet.data[1]);      if(this.store) this.passToStore('SOCKET_'+packet.data[0], [ ...packet.data.slice(1)])    };    // 這里跟上面意思應該是一樣的,我很好奇為什么要分開寫,難道上面的寫法不會監聽到下面的信號?    // 然后這里用一個變量暫存this    // 但是下面都是箭頭函數了,我覺得沒必要,畢竟箭頭函數會自動綁定父級上下文的this    let _this = this;    ["connect", "error", "disconnect", "reconnect", "reconnect_attempt", "reconnecting", "reconnect_error", "reconnect_failed", "connect_error", "connect_timeout", "connecting", "ping", "pong"]      .forEach((value) => {        _this.Socket.on(value, (data) => {          Emitter.emit(value, data);          if(_this.store) _this.passToStore('SOCKET_'+value, data)        })      })  }這里就是有點類似重載onevent這個函數了,監聽到事件后,將數據拆包,然后通知執行回調和傳遞給store。大體的邏輯是這樣子。然后這代碼實現有兩部分,第一部分和第二部分邏輯基本一樣。只是分開寫。(其實我也不是很懂啦,如果很有必要的話,我猜第一部分的寫法還監聽不了第二部分的事件吧,所以要另外監聽)。最后只剩下一個passToStore了,其實也很容易懂
 passToStore(event, payload){   // 如果事件不是以SOCKET_開頭的就不用管了   if(!event.startsWith('SOCKET_')) return   // 這里遍歷vuex的store中的mutations   for(let namespaced in this.store._mutations) {     // 下面的操作是因為,如果store中有module是開了namespaced的,會在mutation的名字前加上 xxx/     // 這里將mutation的名字拿出來     let mutation = namespaced.split('/').pop()     // 如果名字和事件是全等的,那就發起一個commit去執行這個mutation     // 也因此,mutation的名字一定得是 SOCKET_開頭的了     if(mutation === event.toUpperCase()) this.store.commit(namespaced, payload)   }   // 這里類似上面   for(let namespaced in this.store._actions) {     let action = namespaced.split('/').pop()     // 這里強制要求了action的名字要以 socket_ 開頭     if(!action.startsWith('socket_')) continue     // 這里就是將事件轉成駝峰式     let camelcased = 'socket_'+event         .replace('SOCKET_', '')         .replace(/^([A-Z])|[/W/s_]+(/w)/g, (match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase())     // 如果action和事件全等,那就發起這個action     if(action === camelcased) this.store.dispatch(namespaced, payload)   } }passToStore嘛其實就是做兩個事情,一個是獲取與該事件對應的mutation,然后發起一個commit,一個是獲取與該事件對應的action,然后dispatch。只是這里的實現對mutations和actions的命名有了要求,比如mutations的命名一定得是SOCKET_開頭,action就是一個得socket_開頭,然后還得是駝峰式命名。
最后
首先,這個源碼是不是略有點簡單,哈哈哈,不過,能給你們一些幫助,我覺得也挺好的 
然后,就是如果上面我說的有是很對的,請大家去這里發issue或者直接評論吧 
最后,源碼的詳細的注釋在這里
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持武林網。
新聞熱點
疑難解答