websoket
- 在vuex中定义两个连接的状态,第一个状态变量是IsOpen,这个代表的是websoket连接的状态,第二个变量状态是SocketTask,这个代表的是websoket监听的状态,比如websoket监听服务器发送给客户端信息状态,客户端发送给服务器端的信息状态,websoket关闭的状态,websoket连接成功之后的状态,IsOnline变量代表的是当前用户是否处于上线状态;
// Socket连接状态
IsOpen:false,
// SocketTask
SocketTask:false,
// 是否上线
IsOnline:false,
- 开启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)
})
},
- 在vue中调用actions异步openSocket方法连接websoket,目的是为了用户一开始进入页面就要websoket处于连接并且被监听的状态;
this.$store.dispatch('openSocket')
- 在监听服务器给客户端传输信息回调函数中,服务器传输过来的数据是字符串,所以我们要转换成为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
})
}
- 下面则讲解一下用户登录成功之后,接收到服务器端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 // 接收到的时间
}
- 处理接收信息,首先要判断用户服务器传递过来数据中的类型是否是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)
})
- 在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()
});
},
- 在mutations中创建聊天对象,聊天对象的场景必须是在用户进入聊天页面的时候才创建聊天对象,当用户退出聊天页面就应该关闭聊天对象;
// 当前聊天对象(进入聊天页面获取)
ToUser:{
user_id:0,
username:"",
userpic:""
},
// 创建聊天对象
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'
});
}
}
});
}
- 发送消息逻辑实现,发送消息是在聊天页面初始化的时候被调用的;
- 把数据组织成为固定的发送格式,然后在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')
}
},
- 读取当前会话,去除未读数,更新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
}