背景
有一個項目,今年12月份開始重構(gòu)泊愧,項目涉及到了socket稳吮。但是socket用的是以前一個開發(fā)人員封裝的包(這個一直被當(dāng)前的成員吐槽為什么不用已經(jīng)千錘百煉的輪子)。因此派撕,趁著這個重構(gòu)的機(jī)會,將vue-socket.io引入睬魂,后端就用socket.io终吼。我也好奇看了看vue-socket.io的源碼(我不會說是因?yàn)檫@個庫的文檔實(shí)在太簡略了,我為了穩(wěn)點(diǎn)去看源碼了解該怎么用)
開始
-
文件架構(gòu)
文件架構(gòu)我們主要看src下的三個文件氯哮,可以看出該庫是用了觀察者模式
- Main.js
// 這里創(chuàng)建一個observe對象际跪,具體做了什么可以看Observer.js文件
let observer = new Observer(connection, store)
// 將socket掛載到了vue的原型上,然后就可以
// 在vue實(shí)例中就可以this.$socket.emit('xxx', {})
Vue.prototype.$socket = observer.Socket;
import store from './yourstore'
Vue.use(VueSocketio, socketio('http://socketserver.com:1923'), store);
我們?nèi)绻褂眠@個庫的時候,一般是這樣寫的代碼(上圖2)喉钢。上圖一的connection和store就分別是圖二的后兩個參數(shù)姆打。意思分別為socket連接的url和vuex的store啦。圖一就是將這兩個參數(shù)傳進(jìn)Observer肠虽,新建了一個observe對象幔戏,然后將observe對象的socket屬性掛載在Vue原型上。那么我們在Vue的實(shí)例中就可以直接 this.$sockets.emit('xxx', {})
了
// ??就是在vue實(shí)例的生命周期做一些操作
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時監(jiān)聽好的socket事件闲延,全部取消監(jiān)聽
* delete this.$option.sockets的某個屬性時痊剖,就會將取消該信號的監(jiān)聽
*/
beforeDestroy(){
let sockets = this.$options['sockets']
if(sockets){
Object.keys(sockets).forEach((key) => {
delete this.$options.sockets[key]
});
}
}
下面就是在Vue實(shí)例的生命周期做一些操作。創(chuàng)建的時候垒玲,將實(shí)例中的$options.sockets
的值先緩存下來陆馁,再將$options.sockets
指向一個proxy對象,這個proxy對象會攔截外界對它的賦值和刪除屬性操作侍匙。這里賦值的時候氮惯,鍵就是socket事件,值就是回調(diào)函數(shù)想暗。賦值時妇汗,就會監(jiān)聽該事件,然后將回調(diào)函數(shù)说莫,放進(jìn)該socket事件對應(yīng)的回調(diào)數(shù)組里杨箭。刪除時,就是取消監(jiān)聽該事件了储狭,將賦值時壓進(jìn)回調(diào)數(shù)組的那個回調(diào)函數(shù)互婿,刪除,表示辽狈,我不監(jiān)聽了慈参。這樣寫法,其實(shí)就跟vue的響應(yīng)式一個道理刮萌。也因此驮配,我們就可以動態(tài)地添加和移除監(jiān)聽socket事件了,比如this.$option.sockets.xxx = () => ()
和 delete this.$option.sockets.xxx
着茸。最后將緩存的值壮锻,依次賦值回去,那么如下圖的寫法就會監(jiān)聽到事件并執(zhí)行回調(diào)函數(shù)了:
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) {
// 回調(diào)函數(shù)類型是回調(diào)函數(shù)才對
if(typeof callback == 'function'){
// 這里就很常見的寫法了猜绣,判斷map中是否已經(jīng)注冊過該事件了
// 如果沒有,就初始化該事件映射的值為空數(shù)組敬特,方便以后直接存入回調(diào)函數(shù)
// 反之掰邢,直接將回調(diào)函數(shù)放入數(shù)組即可
this.listeners.has(label) || this.listeners.set(label, []);
this.listeners.get(label).push({callback: callback, vm: vm});
return true
}
return false
}
其實(shí)很常規(guī)啦,實(shí)現(xiàn)發(fā)布訂閱者模式或者觀察者模式代碼的同學(xué)都很清楚這段代碼的意思伟阔。Emiiter用一個map來存儲事件以及它對應(yīng)的回調(diào)事件數(shù)組尸变。這段代碼先判斷map中是否之前已經(jīng)存儲過了該事件,如果沒有减俏,初始化該事件對應(yīng)的值為空數(shù)組,然后將當(dāng)前的回調(diào)函數(shù)碱工,壓進(jìn)去娃承,反之奏夫,直接壓進(jìn)去。
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;
這里也很簡單啦历筝,獲取該事件對應(yīng)的回調(diào)數(shù)組酗昼。如果不為空,就去尋找需要移除的回調(diào)梳猪,找到后麻削,直接刪除,然后將新的回調(diào)數(shù)組覆蓋原來的那個就可以了
emit
if (listeners && listeners.length) {
listeners.forEach((listener) => {
listener.callback.call(listener.vm,...args)
});
return true;
}
return false;
這里就是監(jiān)聽到事件后春弥,執(zhí)行該事件對應(yīng)的回調(diào)函數(shù)呛哟,注意這里的call,因?yàn)楸O(jiān)聽到事件后我們可能要修改下vue實(shí)例的數(shù)據(jù)或者調(diào)用一些方法匿沛,用過vue的同學(xué)都知道我們都是this.xxx
來調(diào)用的扫责,所以一定得將回調(diào)函數(shù)的this指向vue實(shí)例,這也是為什么存回調(diào)事件時也要把vue實(shí)例存下來的原因逃呼。
- Observer.js
constructor(connection, store) {
// 這里很明白吧鳖孤,就是判斷這個connection是什么類型
// 這里的處理就是你可以傳入一個連接好的socket實(shí)例,也可以是一個url
if(typeof connection == 'string'){
this.Socket = Socket(connection);
}else{
this.Socket = connection
}
// 如果有傳進(jìn)vuex的store可以響應(yīng)在store中寫的mutations和actions
// 這里只是掛載在這個oberver實(shí)例上
if(store) this.store = store;
// 監(jiān)聽抡笼,啟動苏揣!
this.onEvent()
}
這個Observer.js里也主要是寫了一個Observer的class,以上是它的構(gòu)造函數(shù)推姻,構(gòu)造函數(shù)第一件事是判斷connection是不是字符串平匈,如果是就構(gòu)建一個socket實(shí)例,如果不是拾碌,就大概是個socket的實(shí)例了吐葱,然后直接掛載在它的對象實(shí)例上。其實(shí)這里我覺得可以參數(shù)檢查嚴(yán)格點(diǎn)校翔, 比如字符串被人搞怪地可能會傳入一個非法的url弟跑,對吧。這個時候判斷下防症,拋出一個error提醒下也好孟辑,不過應(yīng)該也沒人這么無聊吧,2333蔫敲。然后如果傳入了store饲嗽,也掛在對象實(shí)例上吧。最后就啟動監(jiān)聽事件啦奈嘿。我們看看onEvent的邏輯
onEvent(){
// 監(jiān)聽服務(wù)端發(fā)來的事件貌虾,packet.data是一個數(shù)組
// 第一項是事件,第二個是服務(wù)端傳來的數(shù)據(jù)
// 然后用emit通知訂閱了該信號的回調(diào)函數(shù)執(zhí)行
// 如果有傳入了vuex的store裙犹,將該事件和數(shù)據(jù)傳入passToStore尽狠,執(zhí)行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)])
};
// 這里跟上面意思應(yīng)該是一樣的衔憨,我很好奇為什么要分開寫,難道上面的寫法不會監(jiān)聽到下面的信號袄膏?
// 然后這里用一個變量暫存this
// 但是下面都是箭頭函數(shù)了践图,我覺得沒必要,畢竟箭頭函數(shù)會自動綁定父級上下文的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)
})
})
}
這里就是有點(diǎn)類似重載onevent這個函數(shù)了沉馆,監(jiān)聽到事件后码党,將數(shù)據(jù)拆包,然后通知執(zhí)行回調(diào)和傳遞給store斥黑。大體的邏輯是這樣子揖盘。然后這代碼實(shí)現(xiàn)有兩部分,第一部分和第二部分邏輯基本一樣心赶。只是分開寫扣讼。(其實(shí)我也不是很懂啦,如果很有必要的話缨叫,我猜第一部分的寫法還監(jiān)聽不了第二部分的事件吧椭符,所以要另外監(jiān)聽)。最后只剩下一個passToStore了耻姥,其實(shí)也很容易懂
passToStore(event, payload){
// 如果事件不是以SOCKET_開頭的就不用管了
if(!event.startsWith('SOCKET_')) return
// 這里遍歷vuex的store中的mutations
for(let namespaced in this.store._mutations) {
// 下面的操作是因?yàn)橄郏绻鹲tore中有module是開了namespaced的,會在mutation的名字前加上 xxx/
// 這里將mutation的名字拿出來
let mutation = namespaced.split('/').pop()
// 如果名字和事件是全等的琐簇,那就發(fā)起一個commit去執(zhí)行這個mutation
// 也因此蒸健,mutation的名字一定得是 SOCKET_開頭的了
if(mutation === event.toUpperCase()) this.store.commit(namespaced, payload)
}
// 這里類似上面
for(let namespaced in this.store._actions) {
let action = namespaced.split('/').pop()
// 這里強(qiáng)制要求了action的名字要以 socket_ 開頭
if(!action.startsWith('socket_')) continue
// 這里就是將事件轉(zhuǎn)成駝峰式
let camelcased = 'socket_'+event
.replace('SOCKET_', '')
.replace(/^([A-Z])|[\W\s_]+(\w)/g, (match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase())
// 如果action和事件全等,那就發(fā)起這個action
if(action === camelcased) this.store.dispatch(namespaced, payload)
}
}
passToStore嘛其實(shí)就是做兩個事情婉商,一個是獲取與該事件對應(yīng)的mutation似忧,然后發(fā)起一個commit,一個是獲取與該事件對應(yīng)的action丈秩,然后dispatch盯捌。只是這里的實(shí)現(xiàn)對mutations和actions的命名有了要求,比如mutations的命名一定得是SOCKET_開頭蘑秽,action就是一個得socket_開頭饺著,然后還得是駝峰式命名。
最后
- 首先肠牲,這個源碼是不是略有點(diǎn)簡單幼衰,哈哈哈,不過缀雳,能給你們一些幫助渡嚣,我覺得也挺好的
- 然后,就是如果上面我說的有是很對的,請大家去這里發(fā)issue或者直接評論吧
- 最后严拒,源碼的詳細(xì)的注釋在這里扬绪,歡迎大家提issue,如果能star和fork就更好了裤唠。以后我盡量更新自己閱讀源碼的感悟,大家一起學(xué)習(xí)莹痢。