websoket
- 在vuex中定義兩個連接的狀態(tài)篡殷,第一個狀態(tài)變量是IsOpen怠噪,這個代表的是websoket連接的狀態(tài),第二個變量狀態(tài)是SocketTask,這個代表的是websoket監(jiān)聽的狀態(tài)次坡,比如websoket監(jiān)聽服務器發(fā)送給客戶端信息狀態(tài)爱只,客戶端發(fā)送給服務器端的信息狀態(tài)张咳,websoket關閉的狀態(tài)鼎俘,websoket連接成功之后的狀態(tài),IsOnline變量代表的是當前用戶是否處于上線狀態(tài)剖煌;
// Socket連接狀態(tài)
IsOpen:false,
// SocketTask
SocketTask:false,
// 是否上線
IsOnline:false,
- 開啟websoket材鹦,開啟之前先判斷IsOpen是否為false,如果不為false則直接return耕姊,然后調用uniapp的官方uni.connectSocket這個api桶唐,他的作用是創(chuàng)建一個 WebSocket 連接,他默認會返回一個socketTask對象茉兰,我們在這個對象中可以監(jiān)聽到websoket的開啟狀態(tài)尤泽,websoket的關閉狀態(tài),websoket的錯誤狀態(tài),監(jiān)聽到服務器向客戶端傳輸數據的狀態(tài)坯约,服務器給客戶端傳輸的數據都會保存到e這個對象中里面熊咽,在監(jiān)聽這些關閉,開啟和錯誤狀態(tài)之前我們必須先判斷服務器有沒有給我們返回socketTask對象闹丐,如果沒有返回則代表服務器連接websoket失敗了横殴,如果返回了socketTask對象則代表服務器和客戶端建立websoket成功了;
openSocket({ state,dispatch }){
// 防止重復連接
if(state.IsOpen) return
// 連接
state.SocketTask = uni.connectSocket({
url: $C.websocketUrl,
complete: ()=> {}
});
if (!state.SocketTask) return;
// 監(jiān)聽開啟
state.SocketTask.onOpen(()=>{
// 將連接狀態(tài)設為已連接
console.log('將連接狀態(tài)設為已連接');
state.IsOpen = true
})
// 監(jiān)聽關閉
state.SocketTask.onClose(()=>{
console.log('連接已關閉');
state.IsOpen = false;
state.SocketTask = false;
state.IsOnline = false
// 清空會話列表
// 更新未讀數提示
})
// 監(jiān)聽錯誤
state.SocketTask.onError(()=>{
console.log('連接錯誤');
state.IsOpen = false;
state.SocketTask = false;
state.IsOnline = false
})
// 監(jiān)聽接收信息
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)
})
},
- 在vue中調用actions異步openSocket方法連接websoket卿拴,目的是為了用戶一開始進入頁面就要websoket處于連接并且被監(jiān)聽的狀態(tài)衫仑;
this.$store.dispatch('openSocket')
- 在監(jiān)聽服務器給客戶端傳輸信息回調函數中,服務器傳輸過來的數據是字符串堕花,所以我們要轉換成為json格式的文狱;
- 在監(jiān)聽服務器給客戶端傳輸信息的回調函數中后,我們需要把登錄用戶的id和websocket服務器分配的客戶端id進行綁定缘挽,首先我們需要調用userBind函數請求服務器端來進行登錄用戶id和websocket服務器分配的客戶端id綁定一起瞄崇,這樣做的目的是為了讓不同的用戶開啟多個聊天窗口的時候可以綁定的都是同一個id,還有如果用戶是多端登錄的壕曼,也可以把用戶明確的綁定到同一個id苏研,這樣就可以區(qū)別用戶的真實身份;
// 監(jiān)聽接收信息
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'){
// 改為上線狀態(tài)
state.IsOnline = true
// 初始化會話列表
dispatch('initChatList')
// 獲取未讀信息
dispatch('getUnreadMessages')
}
}).catch(err=>{
// 失敗 退出登錄腮郊,重新鏈接等處理
})
},
- 在綁定成功之后楣富,就開啟了上線的狀態(tài),首先讓state.IsOnline=true伴榔,因為我們的上線 狀態(tài)就是靠它來判定的,第二步就是初始化會話列表庄萎,首先獲取到本地緩存中的所有聊天記錄列表踪少,然后就是需要更新未讀數角標,調用updateTabbarBadge方法獲取當前登錄用戶的所有未讀數糠涛,然后渲染更新到角標中援奢,首先我們會在getter中計算出所有的聊天未讀數,然后獲取到所有的未讀數忍捡,如果當前的未讀數是0集漾,代表用戶未讀信息不存在,那么就調用官方api中的uni.removeTabBarBadge方法刪除掉狡辯砸脊,如果當前的未讀數存在的還是調用官方的api中的uni.setTabBarBadge方法設置對應的角標未讀數具篇,判斷未讀數如果是99了,那么就不需要繼續(xù)增加未讀數凌埂,只需要顯示99+就好了驱显,否則的話就正常顯示實時更新的未讀數就好了;
- 獲取用戶所有的未讀信息,首先我們需要調用http請求獲取到所有的消息隊列埃疫,這個請求是一個post請求伏恐,獲取到數據之后,將數據進行遍歷循環(huán)栓霜,然后調用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
})
}
- 下面則講解一下用戶登錄成功之后,接收到服務器端websoket發(fā)送客戶端的信息兩種不同的場景胳蛮,第一種是和當前聊天對象id12的人處于聊天的模式销凑,第二種就是和當前聊天對象id為12的人處于不聊天的模式;
- 如果是和當前用戶處于聊天的模式鹰霍,就先把服務器返回客戶端的數據渲染到頁面上闻鉴,然后再把數據存儲到緩存當中,存儲的數據為兩種茂洒,一種是chatdetail_17_12孟岛,這個存儲的數據代表的是當前用戶和聊天用戶的所有數據信息,17就是當前登錄用戶的id督勺,12為當前聊天用戶的id渠羞,一種是chatlist17,這個存儲的數據代表的是當前登錄用戶id為17下的所有的聊天用戶信息智哀,這個數據存儲的有當前最新的聊天時間和最新的聊天信息數據次询,還有置頂的聊天信息;
- 如果是和當前用戶沒有處于聊天的模式瓷叫,也是要先把數據緩存到頁面上作為聊天記錄屯吊,第二就是把數據緩存到本地緩存中去,摹菠,存儲的數據為兩種盒卸,一種是chatdetail_17_12,這個存儲的數據代表的是當前用戶和聊天用戶的所有數據信息次氨,17就是當前登錄用戶的id蔽介,12為當前聊天用戶的id,一種是chatlist17煮寡,這個存儲的數據代表的是當前登錄用戶id為17下的所有的聊天用戶信息虹蓄,這個數據存儲的有當前最新的聊天時間和最新的聊天信息數據,還有置頂的聊天信息幸撕,第三種緩存的數據就是總未讀數薇组,這個數據顯示的是聊天用戶發(fā)給用戶的未讀信息
- 只要有一個用戶發(fā)送信息過來,我都會把那個用戶的當前信息存儲到本地緩存中杈帐,存儲緩存中的數據名稱為chatdetail_17_12(這里做一個示例),前面的17代表的是當前登錄用戶id体箕,后面的12代表的是當前聊天用戶的id专钉;
// 接收到的消息格式:
{
to_id:1, // 接收人
from_id:12, // 發(fā)送人
from_username:"某某", // 發(fā)送人昵稱
from_userpic:"http://pic136.nipic.com/1.jpg",
type:"text", // 發(fā)送類型
data:"你好啊", // 發(fā)送內容
time:151235451 // 接收到的時間
}
- 處理接收信息,首先要判斷用戶服務器傳遞過來數據中的類型是否是text累铅,如果是text跃须,就可以調用異步actions中的handleChatMessage方法處理數據,并且把服務器傳遞過來的數據實時的傳過去娃兽;
// 監(jiān)聽接收信息
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)
})
- 在handleChatMessage方法中菇民,處理服務器響應給我們的數據信息;
// 處理接收消息
handleChatMessage({state,dispatch},data){
console.log('處理接收消息',data);
// 全局通知接口
uni.$emit('UserChat',data);
// 存儲到chatdetail
dispatch('updateChatDetailToUser',{
data,
send:false
})
// 更新會話列表
dispatch('updateChatList',{
data,
send:false
})
},
- toId的獲取是根據用戶是否存在發(fā)送的狀態(tài)來判定的,如果用戶聊天對象是處于當前聊天模式的狀態(tài)投储,那么就是發(fā)送信息第练,也就會取state.ToUser.user_id為聊天對象id,如果用戶處于沒有聊天的狀態(tài)玛荞,那么就會取data.from_id為聊天對象id娇掏;
- 調用getChatDetailToUser通過聊天對象id獲取到聊天對象的聊天記錄,在緩存的時候拼接上的key的格式為chatdetail_myId_toId,myId是用戶登錄id勋眯,toId是聊天對象id婴梧,最后進行判斷,如果當前的本地緩存中沒有這條數據客蹋,就返回一個空數組塞蹭;
- 調用formatChatDetailObject把服務器傳輸過來的數據進行格式化處理,在轉換數據格式的時候每一條記錄都要進行判斷一下讶坯,如果是聊天用戶發(fā)送番电,就獲取state.ToUser.user_id
,如果當前不是處于發(fā)送的狀態(tài)下辆琅,就獲取到data.from_id離線狀態(tài)下的聊天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))
},
- 僅接著就是更新會話列表所宰,首先判斷是否是本人發(fā)送绒尊,然后獲取到所有的聊天會話列表信息,根據所有會話列表信息查詢當前的會話是否已經存在于會話列表中仔粥,如果不存在的話婴谱,就創(chuàng)建會話列表蟹但,把接受到信息轉換成為會話消息的數據格式,并且追加頭部谭羔,如果會話列表在被查詢出是存在的話华糖,首先要在state中定義當前聊天的用戶對象ToUser,這個聊天對象主要是記錄當前登錄用戶是和哪個聊天用戶聊天的對象瘟裸,我們通過判斷user_id來查看當前的聊天對象是當前登錄用戶還是聊天用戶客叉,如果user_id=from_id,那么就代表是聊天用戶了话告,那就將當前的會話置頂兼搏,并且未讀數+1,緊接著沙郭,我們需要定義一個聊天會話列表數組佛呻,最后就是把更新完畢的所有會話聊天列表保存到本地存儲中和state.chatList這個數組中,如果當前沒有處于聊天的狀態(tài)當中則應該去更新未讀數角標病线;
// 當前聊天對象(進入聊天頁面獲认胖)
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);
// 是否是本人發(fā)送
let isMySend = send
console.log(isMySend ? '本人發(fā)送的信息' : '不是本人發(fā)送的');
// 獲取之前會話
let chatList = await dispatch('getChatList')
// 判斷是否已存在該會話,存在:將當前會話置頂氧苍;不存在:創(chuàng)建并追加至頭部
let i = chatList.findIndex((v)=>{
return v.user_id == data.to_id || v.user_id == data.from_id;
})
// 不存在,創(chuàng)建會話
if(i === -1){
// 接收到的消息轉會話
let obj = await dispatch('formatChatListObject',{
data,
send
})
// 忽略本人發(fā)送
if (!isMySend) {
obj.noread = 1;
}
console.log('不存在當前會話,創(chuàng)建',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(排除本人發(fā)送消息)
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了浪南,那么就不需要繼續(xù)增加未讀數,只需要顯示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()
});
},
- 在mutations中創(chuàng)建聊天對象,聊天對象的場景必須是在用戶進入聊天頁面的時候才創(chuàng)建聊天對象昂羡,當用戶退出聊天頁面就應該關閉聊天對象絮记;
// 當前聊天對象(進入聊天頁面獲取)
ToUser:{
user_id:0,
username:"",
userpic:""
},
// 創(chuàng)建聊天對象
createToUser(state,ToUser){
state.ToUser = ToUser
},
// 關閉聊天對象
closeToUser(state){
state.ToUser = {
user_id:0,
username:"",
userpic:""
}
},
- 在用戶退出的時候關閉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'
});
}
}
});
}
- 發(fā)送消息邏輯實現怨愤,發(fā)送消息是在聊天頁面初始化的時候被調用的;
- 把數據組織成為固定的發(fā)送格式蛹批,然后在sendChatMessage方法中獲取到撰洗,第二步就是更新聊天對象的消息歷史記錄篮愉,再繼續(xù)更新所有的會話聊天記錄列表,最后返回出去差导;
// 發(fā)送消息
async sendChatMessage({dispatch},data){
console.log('發(fā)送消息',data);
// 組織發(fā)送消息格式
let sendData = await dispatch('formatSendData',data)
console.log('發(fā)送消息數據格式',sendData);
// 更新與某個用戶的消息歷史記錄
dispatch('updateChatDetailToUser',{
data:sendData,
send:true
})
// 更新會話列表
dispatch('updateChatList',{
data:sendData,
send:true
})
return sendData
},
// 組織發(fā)送格式
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);
// 是否是本人發(fā)送
let isMySend = send
console.log(isMySend ? '本人發(fā)送的信息' : '不是本人發(fā)送的');
// 獲取之前會話
let chatList = await dispatch('getChatList')
// 判斷是否已存在該會話试躏,存在:將當前會話置頂;不存在:創(chuàng)建并追加至頭部
let i = chatList.findIndex((v)=>{
return v.user_id == data.to_id || v.user_id == data.from_id;
})
// 不存在,創(chuàng)建會話
if(i === -1){
// 接收到的消息轉會話
let obj = await dispatch('formatChatListObject',{
data,
send
})
// 忽略本人發(fā)送
if (!isMySend) {
obj.noread = 1;
}
console.log('不存在當前會話,創(chuàng)建',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(排除本人發(fā)送消息)
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')
}
},
- 讀取當前會話冗酿,去除未讀數,更新tabbar络断;
- 首先判斷當前的未讀數是否是0裁替,如果是0那么就直接返回就可以了,然后取出所有的聊天記錄列表進行遍歷循環(huán)貌笨,判斷當前的未讀數傳過來的數據中的item.user_id是否和所有的聊天記錄列表的v.user_id相等弱判,如果相等,那就代表用戶已經讀取了當前的會話或者是打開了會話锥惋,就可以讓未讀數直接清0昌腰,遍歷完成之后就調用mutations方法中的saveChatList更新當前所有聊天的會話列表到本地緩存,繼續(xù)調用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
}