前面介绍了Laravel中Websocket基本使用(Workerman) 接下来利用uni-app laravel workman实现一个简单的聊天功能。 聊天功能主要涉及到以下场景
场景一
双方都处于聊天界面
这个时候我们要
- 将聊天数据渲染到页面
- 将产生的聊天数据放到本地存储用于历史记录等
2.1存储当前聊天数据(直接存储 key=chatdetail_当前用户id_聊天对象id
)
2.2存储当前聊天列表 (key=chatlist_当前用户id
) 将当前聊天会话在消息列表置顶,更新最后一条消息,更新时间
如下图
场景二
当前用户不处于聊天界面如: 用户在其他页面或者当前用户正在与其他用户聊天,此时接受到消息 这个时候我们要
- 将消息渲染到聊天列表,展示最后一条消息,消息数量,时间等
- 将聊天数据放到本地存储
2.1存储聊天数据(直接存储 key=chatdetail_当前用户id_发送消息的用户id
)
2.2存储当前聊天列表 (key=chatlist_当前用户id
) 将接受到的消息在消息列表置顶,更新最后一条消息,更新时间,更新tabbat角标展示
发送消息
- 将聊天数据存储到本地存储
1.1存储聊天数据(直接存储 key=chatdetail_当前用户id_聊天对象id
)
1.2存储当前聊天列表 (key=chatlist_当前用户id
) 将当前会话置顶在消息列表置顶,更新最后一条消息,更新时间 1.3请求ajax发送消息
1.4渲染到页面
读取消息
- 写入本地存储
chatlist_当前用户id
:获取将当前会话的未读书清零,相应的减少总未读书(总未读书展示在tabbar)渲染tabbar
可以看到有很多类似的操作,我们可以将其封装成一个聊天对象。 首先新键配置文件 config.js
代码语言:javascript复制export default{
//api请求前缀
webUrl:'http://social.zihanzy.com/api/v1/',
//websocket
websocket:'wss://social.zihanzy.com/wss',
//展示未读信息的tabbar
TabbarIndex:2
}
新键chat.js(聊天相关操作)
代码语言:javascript复制 import Config from './config.js';
import User from './user.js';
export default{
//socket地址
url:Config.websocket,
//连接状态
IsOpen:false,
//SocketTask
SocketTask:false,
//是否上线(会员id绑定客户端id 验证用户身份 通过则绑定)
IsOnline:false,
//当前聊天对象(进入聊天页面获取)
CurrentToUser:{
userid:0,//判断userid是否为0,当前用户处于什么场景下
username:"",
userpic:""
},
//连接
Open(){
if(this.IsOpen)return;//防止重复连接
//连接
this.SocketTask = uni.connectSocket({
url:this.url,
complete:(e)=>{}
});
if(!this.SocketTask)return;
//监听开启
this.SocketTask.onOpen(()=>{
//连接成功将连接状态设置为已连接
this.IsOpen = true;
});
//监听消息
this.Message();
//监听关闭
this.SocketTask.onClose(()=>{
this.IsOpen = false;
this.SocketTask = false;
});
//监听错误
this.SocketTask.onError((e)=>{
this.IsOpen=false;
this.SocketTask = false;
})
},
...
1.我们初始化相关配置,引入相关文件,编写了websocket连接逻辑 当连接成功后我们会将状态设置为已连接。
代码语言:javascript复制...
//用户绑定
async UserBind(client_id){
let res = await this.$.post('chat/bind',{
type:'bind',
client_id:client_id,
},{
header: {
'Accept':'application/json',
'Authorization':'Bearer ' uni.getStorageSync('access_token')
},
})
console.log(res)
//错判断
//成功处理
return this.resultUserBind(res.data)
},
...
2.用户绑定将用户的token和消息类型发送给服务端,服务端会对用户进行验证在执行用户是否上线操作
代码语言:javascript复制 //监听信息
Message(){
//接受服务器消息
this.SocketTask.onMessage((e)=>{
//字符串转换json
let res = JSON.parse(e.data);
/*
{
type:"bind",
status:true//绑定成功
}
*/
//绑定返回结束
if(res.type=='bind')return this.UserBind(res);//服务器返回client-id下一步执行用户绑定
if(res.type!=='text') return;//接受的信息不是信息就return
//全局通知接口
uni.$emit('UserChat',res);
//存储到chatdetail(与某用户的聊天记录)
this.__UpdateChatdetail(res);//(默认接受信息)
//更新chatlist(当前会话置顶,修改chatlist中当前会话的data和time显示
this.__UpdateChatlist(res);
//总未读数 1 修改tabbar信息数
//当前聊天对象与from_id不同 未读数加1
//只要当前用户与某一用户没有处于聊天界面时执行未读书 1
if(this.CurrentToUser.userid!==res.from_id){
this.__UpdateNoReadNum({type:"add"})
}
})
console.log('监听信息')
},
3.当发送给服务器消息后我们就要接受服务器给我们的响应
- 将服务器给我们的响应进行编码
- 判断服务器的响应类型,如果是
bind(绑定)
,我们会调用用户绑定函数(UserBind
)函数 - 如果接受的响应类型是文本
3.1 全局通知接口,使每个页面都能接受到信息
3.2 将聊天记录存储到本地存储,调用(__UpdateChatdetail
)默认是接受信息(发送消息也会调用该函数)
3.3更新消息列表,将当前会话置顶,修改chatlist中当前会话的data和time显示,调用(__UpdateChatlist
)
3.4使总未读消息数 1,调用(__UpdateNoReadNum
)
接下来分别介绍resultUserBind
、__UpdateChatdetail
、__UpdateChatlist
、__UpdateNoReadNum
Message
> resultUserBind
...
//用户绑定结果
resultUserBind(res){
if(res.status && res.type=='bind'){
//该为上线状态
this.IsOnline = true;
//获取总未读数,并且渲染到tabbar的badge
this.initTabbarBadge();
//获取未读信息
// this.getChatMessages();
return;
}
//绑定失败,断开连接
uni.showToast({
title:res.msg,
icon:"none"
})
this.SocketTask.close();
},
...
该函数获取服务器的用户绑定结果
- 如果用户状态正常则使用户上线
- 初始化tabbar的未读总信息角标
initTabbarBadge
- 获取未读的信息
getChatMessages
- 绑定失败断开连接,并展示相关提示
Message
> resultUserBind
> initTabbarBadge
//初始化tabber信息
initTabbarBadge(){
//获取总未读数
console.log('初始化tabber未读数');
let noreadnum = uni.getStorageSync('noreadnum' User.userinfo.id);
if(noreadnum && noreadnum>0){
//设置tabber角标
return uni.setTabBarBadge({
index:Config.TabbarIndex,
text:noreadnum>99?'99 ':noreadnum.toString()
});
}
return uni.removeTabBarBadge({
index:Config.TabbarIndex
})
},
该函数用于初始化tabbar信息 获取总未读数 当未读数>0时渲染tabbar 当未读书=0时 清除tabbar的角标
Message
> resultUserBind
> getChatMessages
...
getChatMessages(){
console.log('test未读')
$.post('chat/get',{},{
header: {
'Accept':'application/json',
'Authorization':'Bearer ' uni.getStorageSync('access_token')
}
}).then(datas=>{
let data = datas.data.data
if(data.length>0){
for(let i = 0;i<data.length;i ){
let msg = data[i]
uni.$emit('UserChat',msg);
//存储到chatdetail(与某用户的聊天记录)
this.__UpdateChatdetail(msg);//(默认接受信息)
//更新chatlist(当前会话置顶,修改chatlist中当前会话的msg和time显示
this.__UpdateChatlist(msg);
//总未读数 1 修改tabbar信息数
//当前聊天对象与from_id不同 未读数加1
// //只要当前用户与某一用户没有处于聊天界面时执行未读书 1
if(this.CurrentToUser.userid!==msg.from_id){
this.__UpdateNoReadNum({type:"add"})
}
}
}
})
},
...
该函数用于获取未读信息,当用户离线时接受到的消息暂存在服务端缓存,当用户重新上线时触发该函数获取到未读消息并广播事件UserChat
使页面获取到未读消息
Message
> __UpdateChatdetail
//存储到chatdetail(与某用户的聊天记录)
__UpdateChatdetail(res,issend = false){
/*发送和接受都要调用函数存储,
1.判断是不是发送 发送 issend=true 接受 issend=false (默认接受信息)
如果是发送userid = 当前要发送用户id
如果是接受userid = 发送消息的用户id
*/
let userid = issend ? this.CurrentToUser.userid : res.from_id;
//获取旧数据
let list = uni.getStorageSync('chatdetail_' User.userinfo.id '_' userid);//与某用户的聊天记录
list = list ? JSON.parse(list):[];
//追加
list.push(this.__format(res,{type:"chatdetail",isme:issend,olddata:list}));
//存储
uni.setStorage({
key:'chatdetail_' User.userinfo.id '_' userid,
data:JSON.stringify(list)
})
},
该函数存储当前用户与某位用户的聊天记录,这里分两种情况接受消息和发送消息都要将消息存储,在Message
函数体里面我们默认是接受消息
- 获取原来的消息历史记录
- 对消息历史记录进行追加,调用了函数
__format
进行数据格式化 - 将追加后的数据进行本地存储
Message
> __UpdateChatdetail
> __format
该函数用于数据的格式化,存储聊天记录和消息列表都会用到此函数
__format(data,options={}){
/**
* options={
type:"chatdetail",//转换类型
olddata:olddata,//旧数据(chatdetail中必填)
isme:true //(true本人,false聊天对象,chatdetail中必填)
}
*/
switch(options.type){
case "chatdetail"://聊天详情记录
let list = options.olddata; //旧数据
let chattime = new Date().getTime();//获取当前时间
let length = list.length;
return {
isme:options.isme,
userpic:options.isme ? User.userinfo.userpic : data.from_userpic,
type:data.type,
data:data.data,
time:chattime,
gstime:chattime,
//Time.gettime.getChatTime(chattime,(length)>0)?list[length-1].time:0)
};
break;
isme
主要用于消息渲染的时候将哪个用户展示在右边如果(isme=true)那么这条消息应该展示在右边
Message
>** UpdateChatlist
**
//更新chatlist (将当前会话置顶,修改chatlist中当前会话的data和time显示)
__UpdateChatlist(res){
//获取旧数据
let chatlist = uni.getStorageSync('chatlist' User.userinfo.id);
chatlist = chatlist ? JSON.parse(chatlist):[];
//判断是否已经存在该会话 ,存在:将当前会话置顶,不存在;追加至头部
let index = chatlist.findIndex((item)=>{
//发送和接受消息的用户id
return item.userid == res.to_id || item.userid == res.from_id;
})
//不存在
if(index == -1){
let obj = this.__format(res,{type:"chatlist"});
//忽略本人发送
if(res.from_id!==User.userinfo.id){
obj.noreadnum=1;
}
chatlist.unshift(obj);
}else{
//存在 将当前会话置顶,修改chatlist中当前会话的data和time显示
chatlist[index].data = res.data;
chatlist[index].type = res.type;
chatlist[index].time = res.time;
//当前聊天对象不是该id ,未读书加1(排除本人发送消息)
if(res.from_id !== User.userinfo.id && this.CurrentToUser.userid!==chatlist[index].userid){
chatlist[index].noreadnum ;//不处在聊天对象中
}
//置顶当前会话
chatlist = this.__toFirst(chatlist,index);
}
//存储到本地
uni.setStorage({
key:'chatlist' User.userinfo.id,
data:JSON.stringify(chatlist)
})
},
该函数用于存储消息列表,并进行消息列表内容的更新、时间更新、最新消息置顶等 包含以下步骤
- 获取之前的消息列表
- 对之前的消息列表进行判断,如果存在则置顶,调用
__toFirst
函数 - 不存在则调用
__format
函数进行数据格式化并将数据存储到消息列表数组头部 - 将列表存储到本地存储
Message
> __UpdateChatlist
> __format
switch(options,type){
...
case "chatlist"://新增会话
let obj = {
userid:data.from_id,
userpic:data.from_userpic,
username:data.from_username,
time:data.time,//最新消息时间戳
data:data.data,
noreadnum:0 //未读数
};
//本人发送的消息
if(data.from_id == User.userinfo.id){
obj.userid = this.CurrentToUser.userid;
obj.userpic = this.CurrentToUser.userpic;
obj.username = this.CurrentToUser.username;
}
return obj;
break;
...
Message
> __UpdateChatlist
> __toFirst
//数组置顶
__toFirst(arr,index){
if(index!=0){
arr.unshift(arr.splice(index,1)[0]);
}
return arr;
}
Message
> __UpdateNoReadNum
__UpdateNoReadNum(options = {}){
//获取总未读数
let noreadnum = uni.getStorageSync('noreadnum' User.userinfo.id);
noreadnum = noreadnum || 0;
//接受信息增加
if(options.type == 'add'){
noreadnum ;
//响铃振动提示
this.__Nofify();
}else{
noreadnum-=options.num;//读取信息减少
}
noreadnum = noreadnum > 0 ? noreadnum : 0;
//修改tabbar信息数
this.__UpdateTabbarBadege(noreadnum);
//存储
uni.setStorage({
key:'noreadnum' User.userinfo.id,
data:noreadnum
})
},
该函数用于计算总的消息未读数,有消息时进行响铃提示(__Nofify
),渲染tabbar等(__UpdateTabbarBadege
),比对最新的数据进行存储
Message
> __UpdateNoReadNum
> __Nofify
//消息提示
__Nofify(){
uni.vibrateLong()
},
Message
> __UpdateNoReadNum
> __UpdateTabbarBadege
__UpdateTabbarBadege(num){
if(num && num > 0){
return uni.setTabBarBadge({
index:Config.TabbarIndex,
text:num > 99 ? '99 ':num.toString()
});
}
return uni.removeTabBarBadge({
index:Config.TabbarIndex
})
}
该函数将未读数渲染到tabbar
Message
函数封装完了,接下来封装发送消息函数
Send
//发送消息
send(data){
//发送的格式
let senddata = this.__format(data,{type:"send"});
//存储到chatdetail
this.__UpdateChatdetail(senddata,true);
//存储到chatlist(将当前会话置顶,修改时间内容)
this.__UpdateChatlist(senddata);
//发送服务器(交由页面做)
return senddata;
},
该函数处理发送消息相关逻辑
- 数据格式化,调用
__format
函数 - 将消息存储到本地存储调用
__UpdateChatdetail
函数,可参照前面的 - 将消息存储到chatlist调用
__UpdateChatlist
,参照前面 - 返回聊天数据,在页面进行ajax请求
Send
> __format
...
case "send":
return {
to_id:this.CurrentToUser.userid,
from_id:User.userinfo.id,
from_username:User.userinfo.username,
from_userpic:User.userinfo.userpic,
type:data.type,
time:new Date().getTime()
}
break;
}
},
接收消息函数 Read
//读取当前会话
Read(item){
if(!item.noreadnum)return;
let chatlist = uni.getStorageSync('chatlist' User.userinfo.id);
chatlist = chatlist ? JSON.parse(chatlist):[];
//拿到当前会话
let index = chatlist.findIndex((value)=>{
return value.userid == item.userid;
});
let oldnoreadnum = chatlist[index].noreadnum
//会话存在 未读消息=0
if(index!==-1){
chatlist[index].noreadnum = 0;
//存储
uni.setStorage({
key:"chatlist" User.userinfo.id,
data:JSON.stringify(chatlist)
})
//更新tabber的角标
this.__UpdateNoReadNum({type:"read",num:item.noreadnum});
}
},
该函数用于读取消息,主要包含以下
- 获取旧数据
- 如果该会话存在则使为读消息数清零,更新消息列表
- 重新渲染tabbar
到此chat对象封装完成,移步【聊天实现】