業(yè)務場景
項目是一個單頁面web應用,有一個基礎(chǔ)的websocket服務,用于和服務器通信。剛開始主要有兩個作用:
- 賬號防重復登陸。當賬號在另外的地方登陸時,websocket收到服務器消息,網(wǎng)站即時彈窗提示用戶“當前賬號在另外一個地方登陸”,然后網(wǎng)站清除登錄信息并跳轉(zhuǎn)到登錄頁面。
- 當打開送貨管理界面時,若有新的送貨請求,websocket收到服務器消息,即時提示用戶“您有新的送貨請求,請及時處理”。若沒有打開送貨管理界面,不提示。
起初是這樣的實現(xiàn)的:
//BasciWebsocket.js
websocket.onmessage = function (event) {
try{
let {data} = event;
let _data = JSON.parse(data);
switch(_data.type){
case 100:
Modal.warning({
title: '賬號登錄提醒',
content: "您的賬號已經(jīng)在另外的地方進行登陸,請確認是否您本人操作。如有異常,請聯(lián)系公司系統(tǒng)管理員。",
okText: '知道了',
onOk: ()=> {
tool.clearUserCookie();
window.location = "/login";
}
});
break;
case 101:
let isPageShow = tool.ifDeliveryPageShow();
if(isPageShow){
//提示“您有新的送貨請求,請及時處理”
}
break;
}
}catch(e){
}
};
看起來并沒有什么問題。但是,隨著項目越來越復雜,websocket負責通知的消息也越來越豐富。接收到消息后,往往還要改變具體的頁面展現(xiàn),跟具體的頁面的關(guān)聯(lián)性非常大。比如,用戶停留在訂單詳情頁時,如果收到新的訂單反饋,那么訂單詳情頁的評論列表區(qū)域就要動畫出現(xiàn)新的反饋信息。
這個時候,你會發(fā)現(xiàn),把這類跟具體頁面強關(guān)聯(lián)的邏輯寫在基礎(chǔ)的BasciWebsocket.js文件里不科學,BasciWebsocket.js也變得很臃腫,維護起來有些費勁。問題就在于基礎(chǔ)的websocket服務與每個頁面的消息處理邏輯不夠解耦。
不難看出,在這個業(yè)務場景里,websocket服務其實就是一個消息發(fā)布者,而這些具體頁面則是消息訂閱者。很自然地,我想到了一根救命稻草——經(jīng)典的發(fā)布-訂閱模式(又叫觀察者模式)。
觀察者模式
觀察者模式定義對象間的一種一對多的依賴關(guān)系,當一個對象的狀態(tài)發(fā)生改變時,所有依賴它的對象都將得到通知。
如何實現(xiàn)觀察者模式?
- 指定好誰充當發(fā)布者(websocket服務);
- 為發(fā)布者添加一個緩存列表,用于存放回調(diào)函數(shù)以便通知訂閱者(具體頁面);
- 發(fā)布者對外提供消息訂閱/退訂方法,實質(zhì)就是改變緩存列表的內(nèi)容;
- 發(fā)布消息時,發(fā)布者會遍歷這個緩存列表,一次觸發(fā)里面存放的訂閱者回調(diào)函數(shù);
實踐
首先,把BasciWebsocket.js變身為消息發(fā)布者:
export default class BasicWebsocket{
constructor() {
if(typeof BasicWebsocket.instance === 'object') {
return BasicWebsocket.instance;
}
//緩存列表
this.listeners = {};
this.init();
BasicWebsocket.instance = this;
}
//消息訂閱
addListener = (key,func)=>{
this.listeners[key] = func;
};
//取消訂閱
removeListener = (key)=>{
delete this.listeners[key];
};
init = ()=>{
//獲取websocket對象,全局唯一
let websocket = this.getWebsocket();
let that = this;
//監(jiān)聽websocket消息
websocket.onmessage = function (event) {
try{
let {data} = event
let _data = JSON.parse(data);
//遍歷緩存列表, 通知消息訂閱者
let keys = Object.keys(that.listeners);
for(let i = 0,j = keys.length; i < j; i++){
let func = that.listeners[keys[i]];
func(_data);
}
}catch(e){
}
};
};
//其他代碼
}
然后,在發(fā)貨管理頁加載時,可以進行發(fā)貨消息的訂閱;發(fā)貨管理頁卸載時,進行消息退訂。其他頁面如此類推。
class Distribution extends Component{
// 構(gòu)造
constructor(props) {
super(props);
// 初始狀態(tài)
this.state = {
newCount:0
};
this.websocket = new BasicWebsocket();
}
//組件加載完畢
componentDidMount() {
let that = this;
//消息訂閱
this.websocket.addListener("distribution",(d)=>{
if(d.type == 101){
that.showNotification(); //提示“您有新的送貨請求,請及時處理”
//在頁面展示新的發(fā)貨請求數(shù)量
let {count} = d.data;
let count = that.state.newCount + count;
that.setState({
newCount:count
});
}
});
}
//組件即將卸載
componentWillUnmount() {
//消息退訂
this.websocket.removeListener("distribution");
}
}
至此,通過觀察者模式實現(xiàn)了基礎(chǔ)websocket服務和具體頁面邏輯的解耦,代碼可維護性得到提高。