在uniapp中使用websocket實戰的理解和分析

websoket

  1. 在vuex中定義兩個連接的狀態,第一個狀態變量是IsOpen,這個代表的是websoket連接的狀態,第二個變量狀態是SocketTask,這個代表的是websoket監聽的狀態,比如websoket監聽服務器發送給客戶端信息狀態,客戶端發送給服務器端的信息狀態,websoket關閉的狀態,websoket連接成功之后的狀態,IsOnline變量代表的是當前用戶是否處于上線狀態;
// Socket連接狀態
IsOpen:false,
// SocketTask
SocketTask:false,
// 是否上線
IsOnline:false, 
  1. 開啟websoket,開啟之前先判斷IsOpen是否為false,如果不為false則直接return,然后調用uniapp的官方uni.connectSocket這個api,他的作用是創建一個 WebSocket 連接,他默認會返回一個socketTask對象,我們在這個對象中可以監聽到websoket的開啟狀態,websoket的關閉狀態,websoket的錯誤狀態,監聽到服務器向客戶端傳輸數據的狀態,服務器給客戶端傳輸的數據都會保存到e這個對象中里面,在監聽這些關閉,開啟和錯誤狀態之前我們必須先判斷服務器有沒有給我們返回socketTask對象,如果沒有返回則代表服務器連接websoket失敗了,如果返回了socketTask對象則代表服務器和客戶端建立websoket成功了;
openSocket({ state,dispatch }){
        // 防止重復連接
        if(state.IsOpen) return
        // 連接
        state.SocketTask = uni.connectSocket({
            url: $C.websocketUrl,
            complete: ()=> {}
        });
        if (!state.SocketTask) return;
        // 監聽開啟
        state.SocketTask.onOpen(()=>{
            // 將連接狀態設為已連接
            console.log('將連接狀態設為已連接');
            state.IsOpen = true
        })
        // 監聽關閉
        state.SocketTask.onClose(()=>{
            console.log('連接已關閉');
            state.IsOpen = false;
            state.SocketTask = false;
            state.IsOnline = false
            // 清空會話列表
            // 更新未讀數提示
        })
        // 監聽錯誤
        state.SocketTask.onError(()=>{
            console.log('連接錯誤');
            state.IsOpen = false;
            state.SocketTask = false;
            state.IsOnline = false
        })
        // 監聽接收信息
        state.SocketTask.onMessage((e)=>{
            console.log('接收消息',e);
            // 字符串轉json
            let res = JSON.parse(e.data);
            // 綁定返回結果
            if (res.type == 'bind'){
                // 用戶綁定
                return dispatch('userBind',res.data)
            }
            // 處理接收信息
            if (res.type !== 'text') return;

           // 處理接收消息
           dispatch('handleChatMessage',res)
        })
    },
  1. 在vue中調用actions異步openSocket方法連接websoket,目的是為了用戶一開始進入頁面就要websoket處于連接并且被監聽的狀態;
this.$store.dispatch('openSocket')
  1. 在監聽服務器給客戶端傳輸信息回調函數中,服務器傳輸過來的數據是字符串,所以我們要轉換成為json格式的;
  • 在監聽服務器給客戶端傳輸信息的回調函數中后,我們需要把登錄用戶的id和websocket服務器分配的客戶端id進行綁定,首先我們需要調用userBind函數請求服務器端來進行登錄用戶id和websocket服務器分配的客戶端id綁定一起,這樣做的目的是為了讓不同的用戶開啟多個聊天窗口的時候可以綁定的都是同一個id,還有如果用戶是多端登錄的,也可以把用戶明確的綁定到同一個id,這樣就可以區別用戶的真實身份;
 // 監聽接收信息
state.SocketTask.onMessage((e)=>{
    console.log('接收消息',e);
    // 字符串轉json
    let res = JSON.parse(e.data);
    // 綁定返回結果
    if (res.type == 'bind'){
        // 用戶綁定 res.data就是服務器返回的客戶端id
        return dispatch('userBind',res.data)
    }
});
  
 // 用戶綁定
userBind({state,dispatch},client_id){
    $http.post('/chat/bind',{
        type:"bind",
        client_id:client_id
    },{
        token:true
    }).then(data=>{
        console.log('綁定成功',data);
        // 開始上線
        if(data.status && data.type === 'bind'){
            // 改為上線狀態
            state.IsOnline = true
            // 初始化會話列表
            dispatch('initChatList')
            // 獲取未讀信息
            dispatch('getUnreadMessages')
        }
    }).catch(err=>{
        // 失敗 退出登錄,重新鏈接等處理
    })
},
  • 在綁定成功之后,就開啟了上線的狀態,首先讓state.IsOnline=true,因為我們的上線 狀態就是靠它來判定的,第二步就是初始化會話列表,首先獲取到本地緩存中的所有聊天記錄列表,然后就是需要更新未讀數角標,調用updateTabbarBadge方法獲取當前登錄用戶的所有未讀數,然后渲染更新到角標中,首先我們會在getter中計算出所有的聊天未讀數,然后獲取到所有的未讀數,如果當前的未讀數是0,代表用戶未讀信息不存在,那么就調用官方api中的uni.removeTabBarBadge方法刪除掉狡辯,如果當前的未讀數存在的還是調用官方的api中的uni.setTabBarBadge方法設置對應的角標未讀數,判斷未讀數如果是99了,那么就不需要繼續增加未讀數,只需要顯示99+就好了,否則的話就正常顯示實時更新的未讀數就好了;
  • 獲取用戶所有的未讀信息,首先我們需要調用http請求獲取到所有的消息隊列,這個請求是一個post請求,獲取到數據之后,將數據進行遍歷循環,然后調用handleChatMessage方法進行處理數據,handleChatMessage方法存儲數據的邏輯在下面有詳細講解
    // 初始化會話列表
    async initChatList({state,dispatch,commit}){
        state.chatList = await dispatch('getChatList')
        console.log('初始化會話列表',state.chatList);
        dispatch('updateTabbarBadge')
    },
    
    // 獲取所有聊天會話列表
    getChatList({state}){
        let list = uni.getStorageSync('chatlist_'+state.user.id);
        return list ? JSON.parse(list) : []
    }, 
    
    // 更新未讀數
    updateTabbarBadge({state,getters}){
        let total = getters.totalNoread
        console.log('更新未讀數',total);
        // 未讀數為0,移除
        if(total === 0){
            console.log('移除未讀數');
            return uni.removeTabBarBadge({
                index:2
            })
        }
        console.log('設置未讀數',total);
        uni.setTabBarBadge({
            index:2,
            text: total > 99 ? '99+' : total.toString()
        });
    },
    
    
    
    
    // 獲取未讀信息 
    getUnreadMessages({state,dispatch}){
        console.log('獲取未讀信息中...');
        $http.post('/chat/get',{},{
            token:true,
        }).then((data)=>{
            console.log('獲取未讀信息成功',data);
            data.forEach(msg=>{
                // 處理接收消息
                dispatch('handleChatMessage',msg)
            })
        });
    },
    
    // 處理接收消息
    handleChatMessage({state,dispatch},data){
        console.log('處理接收消息',data);
        // 全局通知接口
        uni.$emit('UserChat',data);
        // 存儲到chatdetail
        dispatch('updateChatDetailToUser',{
            data,
            send:false
        })
        // 更新會話列表
        dispatch('updateChatList',{
            data,
            send:false
        })
    }
  1. 下面則講解一下用戶登錄成功之后,接收到服務器端websoket發送客戶端的信息兩種不同的場景,第一種是和當前聊天對象id12的人處于聊天的模式,第二種就是和當前聊天對象id為12的人處于不聊天的模式;
  • 如果是和當前用戶處于聊天的模式,就先把服務器返回客戶端的數據渲染到頁面上,然后再把數據存儲到緩存當中,存儲的數據為兩種,一種是chatdetail_17_12,這個存儲的數據代表的是當前用戶和聊天用戶的所有數據信息,17就是當前登錄用戶的id,12為當前聊天用戶的id,一種是chatlist17,這個存儲的數據代表的是當前登錄用戶id為17下的所有的聊天用戶信息,這個數據存儲的有當前最新的聊天時間和最新的聊天信息數據,還有置頂的聊天信息;
  • 如果是和當前用戶沒有處于聊天的模式,也是要先把數據緩存到頁面上作為聊天記錄,第二就是把數據緩存到本地緩存中去,,存儲的數據為兩種,一種是chatdetail_17_12,這個存儲的數據代表的是當前用戶和聊天用戶的所有數據信息,17就是當前登錄用戶的id,12為當前聊天用戶的id,一種是chatlist17,這個存儲的數據代表的是當前登錄用戶id為17下的所有的聊天用戶信息,這個數據存儲的有當前最新的聊天時間和最新的聊天信息數據,還有置頂的聊天信息,第三種緩存的數據就是總未讀數,這個數據顯示的是聊天用戶發給用戶的未讀信息
  • 只要有一個用戶發送信息過來,我都會把那個用戶的當前信息存儲到本地緩存中,存儲緩存中的數據名稱為chatdetail_17_12(這里做一個示例),前面的17代表的是當前登錄用戶id,后面的12代表的是當前聊天用戶的id;
// 接收到的消息格式:
{
   to_id:1,      // 接收人 
   from_id:12,   // 發送人
   from_username:"某某",  // 發送人昵稱
   from_userpic:"http://pic136.nipic.com/1.jpg",
   type:"text",     // 發送類型
   data:"你好啊",  // 發送內容
   time:151235451   // 接收到的時間
}
  1. 處理接收信息,首先要判斷用戶服務器傳遞過來數據中的類型是否是text,如果是text,就可以調用異步actions中的handleChatMessage方法處理數據,并且把服務器傳遞過來的數據實時的傳過去;
// 監聽接收信息
state.SocketTask.onMessage((e)=>{
    console.log('接收消息',e);
    // 字符串轉json
    let res = JSON.parse(e.data);
    // 綁定返回結果
    if (res.type == 'bind'){
        // 用戶綁定
        return dispatch('userBind',res.data)
    }
    // 處理接收信息
    if (res.type !== 'text') return;
   // 處理接收消息
   dispatch('handleChatMessage',res)
})
  1. 在handleChatMessage方法中,處理服務器響應給我們的數據信息;
// 處理接收消息
handleChatMessage({state,dispatch},data){
    console.log('處理接收消息',data);
    // 全局通知接口
    uni.$emit('UserChat',data);
    // 存儲到chatdetail
    dispatch('updateChatDetailToUser',{
        data,
        send:false
    })
    // 更新會話列表
    dispatch('updateChatList',{
        data,
        send:false
    })
},  
  • toId的獲取是根據用戶是否存在發送的狀態來判定的,如果用戶聊天對象是處于當前聊天模式的狀態,那么就是發送信息,也就會取state.ToUser.user_id為聊天對象id,如果用戶處于沒有聊天的狀態,那么就會取data.from_id為聊天對象id;
  • 調用getChatDetailToUser通過聊天對象id獲取到聊天對象的聊天記錄,在緩存的時候拼接上的key的格式為chatdetail_myId_toId,myId是用戶登錄id,toId是聊天對象id,最后進行判斷,如果當前的本地緩存中沒有這條數據,就返回一個空數組;
  • 調用formatChatDetailObject把服務器傳輸過來的數據進行格式化處理,在轉換數據格式的時候每一條記錄都要進行判斷一下,如果是聊天用戶發送,就獲取state.ToUser.user_id
    ,如果當前不是處于發送的狀態下,就獲取到data.from_id離線狀態下的聊天id,最后返回出去,然后將整個轉換完成的會話列表數據全部推送到list數組中去(如果本地緩存沒有數據將會返回一個空數組,如果本地緩存有數據,將會覆蓋數組);
  • 在mutations中定義saveChatDetail方法對list當前這個聊天對象的數據進行緩存,存儲的key值中的id為chatdetail_myId_toId,myId是用戶登錄id,toId是聊天對象id,最后把數據全部存儲到本地緩存里面去;
// 更新與某個用戶聊天內容列表
async updateChatDetailToUser({state,dispatch,commit},e){
         console.log('更新與某個用戶聊天內容列表',e);
         let data = e.data
         let toId = e.send ? state.ToUser.user_id : data.from_id
         // 獲取與某個用戶聊天內容的歷史記錄
         let list = await dispatch('getChatDetailToUser',toId)
         list.push(await dispatch('formatChatDetailObject',e))
         // 存儲到本地存儲
         commit('saveChatDetail',{
            list,toId
         })
    },
    
    
// 獲取與某個用戶聊天內容列表
getChatDetailToUser({state},toId = 0){
    // chatdetail_[當前用戶id]_[聊天對象id]
    let myId = state.user.id
    toId = toId ? toId : state.ToUser.user_id
    let key = 'chatdetail_'+myId+'_'+toId
    let list = uni.getStorageSync(key)
    return list ? JSON.parse(list) : []
},
    


// 消息轉聊天會話對象
formatChatListObject({state},{data,send}){
    // 接收消息
    return {
        user_id:send ? state.ToUser.user_id : data.from_id,
        avatar:send ? state.ToUser.avatar : data.from_userpic,
        username:send ? state.ToUser.username : data.from_username,
        update_time:data.time, // 最新消息時間戳
        data:data.data,
        noread:0  // 未讀數
    }
},


// 存儲與某個用戶聊天內容列表
saveChatDetail(state,{list,toId}){
    // chatdetail_[當前用戶id]_[聊天對象id]
    let myId = state.user.id
    toId = toId ? toId : state.ToUser.user_id
    let key = 'chatdetail_'+myId+'_'+toId
    uni.setStorageSync(key,JSON.stringify(list))
},
  • 僅接著就是更新會話列表,首先判斷是否是本人發送,然后獲取到所有的聊天會話列表信息,根據所有會話列表信息查詢當前的會話是否已經存在于會話列表中,如果不存在的話,就創建會話列表,把接受到信息轉換成為會話消息的數據格式,并且追加頭部,如果會話列表在被查詢出是存在的話,首先要在state中定義當前聊天的用戶對象ToUser,這個聊天對象主要是記錄當前登錄用戶是和哪個聊天用戶聊天的對象,我們通過判斷user_id來查看當前的聊天對象是當前登錄用戶還是聊天用戶,如果user_id=from_id,那么就代表是聊天用戶了,那就將當前的會話置頂,并且未讀數+1,緊接著,我們需要定義一個聊天會話列表數組,最后就是把更新完畢的所有會話聊天列表保存到本地存儲中和state.chatList這個數組中,如果當前沒有處于聊天的狀態當中則應該去更新未讀數角標;
// 當前聊天對象(進入聊天頁面獲取)
ToUser:{
    user_id:0,
    username:"",
    userpic:""
},


// 所有的聊天會話列表
chatList:[],


// 數組置頂
__toFirst(arr,index){
    if (index != 0) {
        arr.unshift(arr.splice(index,1)[0]);
    }
    return arr;
},


// 存儲會話列表
saveChatList(state,list){
    uni.setStorageSync('chatlist_'+state.user.id,JSON.stringify(list))
},


// 獲取所有聊天會話列表
getChatList({state}){
    let list = uni.getStorageSync('chatlist_'+state.user.id);
    return list ? JSON.parse(list) : []
},
 
 
 
 
// 更新聊天會話列表
async updateChatList({state,dispatch,commit},{data,send}){
        console.log('更新聊天會話列表',data);
        // 是否是本人發送
        let isMySend = send
        console.log(isMySend ? '本人發送的信息' : '不是本人發送的');
        // 獲取之前會話
        let chatList = await dispatch('getChatList')
        // 判斷是否已存在該會話,存在:將當前會話置頂;不存在:創建并追加至頭部
        let i = chatList.findIndex((v)=>{
            return v.user_id == data.to_id || v.user_id == data.from_id;
        })
        // 不存在,創建會話
        if(i === -1){
            // 接收到的消息轉會話
            let obj = await dispatch('formatChatListObject',{
                data,
                send
            })
            // 忽略本人發送
            if (!isMySend) {
                obj.noread = 1;
            }
            console.log('不存在當前會話,創建',obj);
            // 追加頭部
            chatList.unshift(obj);
        } else {
            // 存在:將當前會話置頂,修改當前會話的data和time顯示
            let item = chatList[i]
            item.data = data.data
            item.type = data.type
            item.time = data.time
            // 當前聊天對象不是該id,未讀數+1(排除本人發送消息)
            if(!isMySend && state.ToUser.user_id !== item.user_id){
                item.noread++
            }
            console.log('存在當前會話',item); 
            // 置頂當前會話
            chatList = $U.__toFirst(chatList,i)
        }
        // 存儲到本地存儲
        state.chatList = chatList
        commit('saveChatList',chatList)
        // 不處于聊天當中,更新未讀數
        if(data.from_id !== state.ToUser.user_id && !isMySend){
            await dispatch('updateTabbarBadge')
        }
    },
    
  • 調用updateTabbarBadge方法獲取當前登錄用戶的所有未讀數,然后渲染更新到角標中,首先我們會在getter中計算出所有的聊天未讀數,然后獲取到所有的未讀數,如果當前的未讀數是0,代表用戶未讀信息不存在,那么就調用官方api中的uni.removeTabBarBadge方法刪除掉狡辯,如果當前的未讀數存在的還是調用官方的api中的uni.setTabBarBadge方法設置對應的角標未讀數,判斷未讀數如果是99了,那么就不需要繼續增加未讀數,只需要顯示99+就好了,否則的話就正常顯示實時更新的未讀數就好了;

// 總未讀數
totalNoread(state){
    let total = 0
    state.chatList.forEach(item=>{
        total += item.noread
    })
    return total
}


// 更新未讀數
updateTabbarBadge({state,getters}){
    let total = getters.totalNoread
    console.log('更新未讀數',total);
    // 未讀數為0,移除
    if(total === 0){
        console.log('移除未讀數');
        return uni.removeTabBarBadge({
            index:2
        })
    }
    console.log('設置未讀數',total);
    uni.setTabBarBadge({
        index:2,
        text: total > 99 ? '99+' : total.toString()
    });
},
  1. 在mutations中創建聊天對象,聊天對象的場景必須是在用戶進入聊天頁面的時候才創建聊天對象,當用戶退出聊天頁面就應該關閉聊天對象;
// 當前聊天對象(進入聊天頁面獲取)
ToUser:{
        user_id:0,
        username:"",
        userpic:""
},

// 創建聊天對象
createToUser(state,ToUser){
    state.ToUser = ToUser
},
// 關閉聊天對象
closeToUser(state){
    state.ToUser = {
        user_id:0, 
        username:"",
        userpic:""
    }
},
  1. 在用戶退出的時候關閉socket;
// 關閉socket
closeSocket({state}){
    if (state.IsOpen){
        state.SocketTask.close();
    }
},

// 退出登錄
logout(){
    uni.showModal({
        content: '是否要退出登錄',
        success: (res)=> {
            if (res.confirm) {
                this.$store.commit('logout')
                // 關閉socket
                this.$store.dispatch('closeSocket')
                uni.navigateBack({
                    delta: 1
                });
                uni.showToast({
                    title: '退出登錄成功',
                    icon: 'none'
                });
            } 
        }
    });
}
  1. 發送消息邏輯實現,發送消息是在聊天頁面初始化的時候被調用的;
  • 把數據組織成為固定的發送格式,然后在sendChatMessage方法中獲取到,第二步就是更新聊天對象的消息歷史記錄,再繼續更新所有的會話聊天記錄列表,最后返回出去;
// 發送消息
async sendChatMessage({dispatch},data){
    console.log('發送消息',data);
    // 組織發送消息格式
    let sendData = await dispatch('formatSendData',data)
    console.log('發送消息數據格式',sendData);
   // 更新與某個用戶的消息歷史記錄
   dispatch('updateChatDetailToUser',{
     data:sendData,
     send:true
   })
   // 更新會話列表
   dispatch('updateChatList',{
       data:sendData,
       send:true
   })
   return sendData
},


// 組織發送格式
formatSendData({state},data){
    return {
        to_id:state.ToUser.user_id,
        from_id:state.user.id,
        from_username:state.user.username,
        from_userpic:state.user.userpic ? state.user.userpic : '/static/default.jpg',
        type:data.type,
        data:data.data,
        time:new Date().getTime()
    }
},


// 更新與某個用戶聊天內容列表
async updateChatDetailToUser({state,dispatch,commit},e){
     console.log('更新與某個用戶聊天內容列表',e);
     let data = e.data
     let toId = e.send ? state.ToUser.user_id : data.from_id
     // 獲取與某個用戶聊天內容的歷史記錄
     let list = await dispatch('getChatDetailToUser',toId)
     list.push(await dispatch('formatChatDetailObject',e))
     // 存儲到本地存儲
     commit('saveChatDetail',{
        list,toId
     })
},



// 更新聊天會話列表
async updateChatList({state,dispatch,commit},{data,send}){
    console.log('更新聊天會話列表',data);
    // 是否是本人發送
    let isMySend = send
    console.log(isMySend ? '本人發送的信息' : '不是本人發送的');
    // 獲取之前會話
    let chatList = await dispatch('getChatList')
    // 判斷是否已存在該會話,存在:將當前會話置頂;不存在:創建并追加至頭部
    let i = chatList.findIndex((v)=>{
        return v.user_id == data.to_id || v.user_id == data.from_id;
    })
    // 不存在,創建會話
    if(i === -1){
        // 接收到的消息轉會話
        let obj = await dispatch('formatChatListObject',{
            data,
            send
        })
        // 忽略本人發送
        if (!isMySend) {
            obj.noread = 1;
        }
        console.log('不存在當前會話,創建',obj);
        // 追加頭部
        chatList.unshift(obj);
    } else {
        // 存在:將當前會話置頂,修改當前會話的data和time顯示
        let item = chatList[i]
        item.data = data.data
        item.type = data.type
        item.time = data.time
        // 當前聊天對象不是該id,未讀數+1(排除本人發送消息)
        if(!isMySend && state.ToUser.user_id !== item.user_id){
            item.noread++
        }
        console.log('存在當前會話',item); 
        // 置頂當前會話
        chatList = $U.__toFirst(chatList,i)
    }
    // 存儲到本地存儲
    state.chatList = chatList
    commit('saveChatList',chatList)
    // 不處于聊天當中,更新未讀數
    if(data.from_id !== state.ToUser.user_id && !isMySend){
        await dispatch('updateTabbarBadge')
    }
},

  1. 讀取當前會話,去除未讀數,更新tabbar;
  • 首先判斷當前的未讀數是否是0,如果是0那么就直接返回就可以了,然后取出所有的聊天記錄列表進行遍歷循環,判斷當前的未讀數傳過來的數據中的item.user_id是否和所有的聊天記錄列表的v.user_id相等,如果相等,那就代表用戶已經讀取了當前的會話或者是打開了會話,就可以讓未讀數直接清0,遍歷完成之后就調用mutations方法中的saveChatList更新當前所有聊天的會話列表到本地緩存,繼續調用updateTabbarBadge方法更新未讀數;
// 讀取當前會話(去除未讀數,更新tabbar)
readChatMessage({state,commit,dispatch},item){
    console.log('讀取當前會話(去除未讀數,更新tabbar)',item);
    // 沒有未讀信息
    if (item.noread === 0) return;
    // 拿到當前會話 設置未讀數為0
    state.chatList.forEach((v)=>{
        if(v.user_id == item.user_id){
            v.noread = 0
        }
    })
    // 存儲
    commit('saveChatList',state.chatList)
    // 更新未讀數
    dispatch('updateTabbarBadge')
},

//返回的數據格式
    {
        "user_id": 331,
        "avatar": "/static/default.jpg",
        "username": "13450772004",
        "update_time": 1578216988,
        "data": "看看有么有移除",
        "noread": 0,
        "type": "text",
        "time": 1578226151777
    }
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。