前端刚入职小白,记录websocket在vue前端的实际应用。
1:websocket简单介绍
首先,我们需要简单了解一下websocket:
HTTP 协议是一种无状态的、无连接的、单向的应用层协议。它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理,HTTP 协议无法实现服务器主动向客户端发起消息。
这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数 Web 应用程序将通过频繁的异步 JavaScript 和 XML(AJAX)请求实现长轮询。轮询的效率低,非常浪费资源。
Websocket应运而生,WebSocket 连接允许客户端和服务器之间进行全双工通信,以便任一方都可以通过建立的连接将数据推送到另一端。WebSocket 只需要建立一次连接,就可以一直保持连接状态。这相比于轮询方式的不停建立连接显然效率要大大提高。
2:websocket的实现
其实说到底就是将服务器发送过来的数据在通过websocket接收函数时,通过字段判断和操作
1:是正在和我聊天的人发的还是别人?
2:收到的未读信息数量?
3:我发送的人的标识ID。
4:我要通过一个方法来让后端知道我已经读取了信息。
5:也要注意发送和接收数据的时间。
话不多说,上代码:
<script>
export default {
data() {
return {
selfId: 0,
// 在线状态
state: 1,
goodsID:0,
recenttext:'',
selfListData: {},
//搜索用户
search: "",
//用户列表渲染数据
userListData: [],
//用户点击选中变色
act: 0,
// 加号弹框
dialogVisible: false,
//模拟花间一壶酒用户的历史信息
userInfoList2: [],
//历史信息
userInfoList: [],
//输入框
textarea: "",
//滚动条距离顶部距离
scrollTop: 0,
//发送和输入显隐
isshow: 0,
websock: null,
page: 1,
page1: 1,
urlhh: '',
abcd: 0,
index: -1,
dialogInfo:{},
firstRunFlag: true,
other_id:0,
category:0,
isUpdate:true,
};
},
created() {
this.initWebSocket()
},
destroyed() {
this.websock.close() //离开路由之后断开websocket连接
},
methods: {
initWebSocket() {
// token为访问令牌,一般登录成功,就会返回token
const token = window.sessionStorage.getItem('token')
// 其中线上的链接为wss://,在本地上的链接为ws://
const wsuri = "wss://www.xxxxxxxx.com:8002/chat_ws/"
// const wsuri = `ws://192.168.0.0:9000/chat_ws/`
// 建立websocket连接
this.websock = new WebSocket(wsuri, [token])
// websocket接收信息
this.websock.onmessage = this.websocketonmessage
// 打开websocket
this.websock.onopen = this.websocketonopen
// websocket连接错误时重连
this.websock.onerror = this.websocketonerror
},
websocketonopen() {
window.console.log('连接成功')
},
websocketonerror() {
//连接建立失败重连
this.initWebSocket()
},
websocketonmessage(e) {
//数据接收,首先要解析服务器传过来的数据
const redata = JSON.parse(e.data)
if (redata.msg_type==3) {
// msg_type字段为3时,代表服务器给我发了文本信息了,注意是明确文本信息了
// 例如对方发送 “你好” 给我时,msg_type就为3,text:'你好'
// 下面这个if判断条件
// this.dialogInfo.other_userid的意思是我跟这个人的对话框的ID
// 比如我跟小明正在聊天,那我跟小明的对话框ID就假设为1
// 此时我收到websocket服务器发来的文本信息text:在吗?,sender:1
// 此时就意味着是小明给我发的信息,如果sender不是1,就代表着不是小明回我的
if(this.dialogInfo.other_userid==redata.sender){
// 如果跟我正在聊天的人给我回消息,下面这个代码,在此页面不影响
// 这个传值的意思就是,将跟我聊天的对话框ID传递给其他组件或页面,可以用来判断未读信息条数,如果收到的信息的sender和我传递过去的ID相等,意味着未读信息就不需要加一
this.$bus.$emit("hi",redata.sender);
}
// 当接收到数据时,就会更新消息列表,为防止没有对话框的用户不在信息列表,
// 同时将该用户的ID记录下来,并记录已经更新过消息列表
if(this.firstRunFlag==true){
// 如果不是正在聊天的人给我回消息,就更新消息列表,为防止没有对话框的用户不在信息列表,
if(this.dialogInfo.other_userid!=redata.sender){
// getChatmessage()该函数为获取用户的列表,比如小明,小红等
this.getChatmessage()
}
this.other_id=redata.sender
this.firstRunFlag=false
}
// 当继续接受到数据时,由于已经记录过更新消息列表了,所以上一步不会执行
// 当接收的数据如果和上次接收的数据发送ID相同时,也不会更新消息列表,因为该用户一定会有对话框在消息列表
// 当接收的数据如果和上次接收的数据发送ID不相同时,即另外一个用户给我发消息时,
// 又要更新消息列表,同样是防止没有对话框的用户不在信息列表
if(this.other_id!=redata.sender){
if(this.dialogInfo.other_userid!=redata.sender){
this.getChatmessage()
}
this.firstRunFlag=true
}
//聊天框的人给你发消息时
if(this.dialogInfo.other_userid==redata.sender){
// userInfoList为信息列表
this.userInfoList.push({
url: this.urlhh, // url为用户的头像,
info: redata.text, // 返回的文本信息
timer: this.dealDate(), // 收到信息的时间
position: "left", // 是对方给我发送信息,所以该信息应该是展示在左边
});
}
// 这个for循环的意思是,遍历整个用户列表,找到发送的人的对话列表,然后将最近的信息和时间进行更新
for(let i=0;i<this.userListData.length;i++){
if(this.userListData[i].other_userid==redata.sender){
this.userListData[i].last_message.text=redata.text,
this.userListData[i].last_message.created=this.dealDate(new Date())
break
}
}
// 收到信息就把信息列表的滚动条置底
this.$nextTick(() => { // 一定要用nextTick
this.setPageScrollTo();
//页面滚动条距离顶部高度等于这个盒子的高度
this.$refs.scrollBox.scrollTop = this.$refs.scrollBox.scrollHeight;
})
} else if (redata.msg_type==2) {
window.console.log('下线')
} else if (redata.msg_type==1) {
window.console.log('上线')
}else if (redata.msg_type==9) {
// unread_count为未读消息个数,msg_type为9,返回未读信息个数
if(this.dialogInfo.other_userid==redata.sender&&redata.unread_count!=0){
// 如果我正在跟小明聊天,他又给我发个信息,我这个方法不能自动已读,只能手动发送信息,来告诉服务器我已读了
let actions = {msg_type:6,user_pk:this.dialogInfo.other_userid,random_id:-5,dialog_id:this.dialogInfo.id}
this.websocketsend(JSON.stringify(actions))
// 上述就是手动发送已读,msg_type为6向服务器发送已读信息
// for循环遍历用户列表,如果发送者的ID是和正在跟我聊天人的ID相等,就强制将用户列表上的跟我聊天的未读信息为0
for(let i=0;i<this.userListData.length;i++){
if(this.userListData[i].other_userid==redata.sender){
// this.userListData[i].unread_count=0
let user = this.userListData[i]
user.unread_count=0
this.$set(this.userListData, i, user);
break
}
}
}
for(let i=0;i<this.userListData.length;i++){
if(this.userListData[i].other_userid==redata.sender){
if(this.userListData[i].other_userid==this.dialogInfo.other_userid){
// for循环遍历用户列表,如果发送者的ID是和正在跟我聊天人的ID相等,就强制将用户列表上的跟我聊天的未读信息为0
// this.userListData[i].unread_count=0
let user = this.userListData[i]
user.unread_count=0
this.$set(this.userListData, i, user);
}else{
// 如果发送者的ID和正在跟我聊天人的ID不相等,将用户列表上的那个人的未读信息重新渲染成最新的
this.userListData[i].unread_count=redata.unread_count
break
}
}
}
}
},
websocketsend(Data) {
//数据发送
this.websock.send(Data)
},
websocketclose() {
//关闭
},
//获取用户自己的信息
getSelf() {
//getUserDetail接口为自己写的获取自己的用户信息的接口,如头像,昵称,ID等
getUserDetail().then((response) => {
this.selfListData = response.data
}).catch(function () {
});
},
load(){
this.page1++
//获取下一页聊天列表
this.getChatnextmessage()
},
// 滚动条置底时触发
scrollEvent (e) {
if (e.srcElement.scrollTop + e.srcElement.clientHeight + 1 > e.srcElement.scrollHeight) {
//当isUpdate为true时,表示用户列表信息还存在,就要继续获取下一页,当isUpdate为false时,用户信息列表没有更多了
if(this.isUpdate) {
this.load();
} else {
this.$message("消息列表已加载完毕");
}
}
},
//获取下一页列表聊天数据
getChatnextmessage() {
return new Promise(resolve=>{
// chatexport为自己写的获取用户信息列表的接口,我这里是一次传20条数据
chatexport({
page:this.page1
}).then((response)=> {
if(response.data.results.length<20){
// 如果返回的信息列表条数小于20,就代表着不会有更多的信息了
this.isUpdate=false
}
let userListDatanext = response.data.results
userListDatanext.forEach(item=>{
if(item.other_userimage==null){
item.other_userimage='这里放上匿名用户的头像'
}
// 处理时间的格式
item.last_message.created = this.dealDate(item.last_message.created)
})
// 将获取的下一页数据合并到用户列表数组中
this.userListData = this.userListData.concat(userListDatanext);
resolve()
}).catch(function (error) {
window.console.log(error);
resolve()
});
})
},
//获取全部渲染到列表聊天数据
getChatmessage() {
return new Promise(resolve=>{
// 滚动条置顶
this.$nextTick(() => {
let scrollEl = this.$refs.mianscroll;
scrollEl.scrollTo({ top: 0, behavior: 'smooth' });
});
this.isUpdate=true
this.page1=1
//原理和上个函数类似
chatexport({
page:this.page1
}).then((response)=> {
if(response.data.results.length<20){
this.isUpdate=false
}
this.userListData = response.data.results
this.userListData.forEach(item=>{
if(item.other_userimage==null){
item.other_userimage='这里放上匿名用户的头像'
}
item.last_message.created = this.dealDate(item.last_message.created)
})
resolve()
}).catch(function (error) {
window.console.log(error);
resolve()
});
})
},
//获取个人聊天数据
getPriChatmessage() {
return new Promise((resolve)=>{
//chatPriMessage为自己写的获取自己和别人的聊天记录,一次传20条数据
chatPriMessage({
dialog_with_id:this.act,//传值dialog_with_id为聊天对象的ID,我这里就是传对话框的ID,其实就是唯一识别就行
page:this.page
}).then((response)=> {
if(response.data.results.length==0){
//比如有40条聊天数据,获取第三页就为0了,就不能在继续获取了
this.abcd=1
//abcd为标识,为1时,不能再继续获取聊天数据了
resolve()
// 长度为0时,没数据了就return
return
}
response.data.results.forEach(item=>{
//is_out为true时为我发的信息,放右边
if(item.is_out){
//username字段可有可无(全文都是)
this.userInfoList.unshift({
url: this.selfListData.wechat_headimgurl,
username: this.selfListData.name,
info: item.text,
timer: this.dealDate(item.created),//created字段为时间
position: "right",
});
resolve()
}else{
//is_out为false时为聊天对象发的信息,放左边
if(item.recipient_url){
this.urlhh=item.recipient_url
}else{
this.urlhh='这里放匿名图片链接'
}
this.userInfoList.unshift({
url: this.urlhh,
username: item.recipient_username,
info: item.text,
timer: this.dealDate(item.created),
position: "left",
});
resolve()
}
})
}).catch(function (error) {
window.console.log(error);
});
})
},
//获取个人最近感兴趣聊天数据(可有可无)
getRecentmessage() {
return new Promise((resolve)=>{
// 这个接口可有可无
getRecentgoods({
dialog_with_id:this.act
}).then((response)=> {
if(response.data.results.length){
this.recenttext=response.data.results[0].text
this.recenttext=this.recenttext.substr(0,50)
this.goodsID=response.data.results[0].object_id
this.category=response.data.results[0].category
}else{
this.recenttext=''
}
resolve()
}).catch(function (error) {
window.console.log(error);
});
})
},
//此功能可有可无
toDetail(){
if(this.category==1){
window.open('要去的链接', "_blank");
}else if(this.category==2){
window.open('要去的链接', "_blank");
}else if(this.category==3){
window.open('要去的链接', "_blank");
}
},
//点击用户
async getAct(val, index) {
this.dialogInfo = val //点击用户列表上的详细信息
this.page=1
this.isshow = 1 //更改样式
this.index=index
this.act=val.other_userid
this.getRecentmessage() //可有可无
this.$bus.$emit("hello", this.userListData[index].unread_count);//传值给其他组件
this.userListData[index].unread_count=0 //未读改为0
let actions = {msg_type:6,user_pk:val.other_userid,random_id:-7,dialog_id:val.id}
this.websocketsend(JSON.stringify(actions))//发送已读信息
// 点击用户切换数据时先清除监听滚动事件,防止出现没有历史数据的用户,滚动条为0,会触发滚动事件
this.$refs.scrollBox.removeEventListener("scroll", this.srTop);
//点击变色
this.userInfoList = [];
await this.getPriChatmessage()
//滚动条置底
this.$nextTick(() => {
this.setPageScrollTo();
this.$refs.scrollBox.scrollTop = this.$refs.scrollBox.scrollHeight;
})
},
//发送
setUp() {
if(this.textarea == "") {
alert("发送信息不能为空!");
return;
}
let actions = {text:this.textarea,msg_type:3,user_pk:this.act,random_id:-7}
this.websocketsend(JSON.stringify(actions)) //发送至服务器
this.userInfoList.push({
url: this.selfListData.wechat_headimgurl,
username: "超人",
info: this.textarea,
timer: this.dealDate(),
position: "right",
});//渲染到信息列表
this.dialogInfo.last_message.text=this.textarea,//更新用户列表最近信息
this.dialogInfo.last_message.created=this.dealDate(new Date()), //更新用户列表最近时间
this.textarea = "";
// 页面滚动到底部
this.$nextTick(() => { // 一定要用nextTick
this.setPageScrollTo();
//页面滚动条距离顶部高度等于这个盒子的高度
this.$refs.scrollBox.scrollTop = this.$refs.scrollBox.scrollHeight;
})
},
// 监听键盘回车阻止换行并发送
handlePushKeyword(event) {
if (event.keyCode === 13) {
event.preventDefault(); // 阻止浏览器默认换行操作
this.setUp(); //发送文本
return false;
}
},
// 监听按的是ctrl + 回车,就换行
lineFeed() {
this.textarea = this.textarea + "\n";
},
//点击icon
extend(val) {
alert("你点击了:" + val);
},
//滚动条默认滚动到最底部
setPageScrollTo() {
//获取中间内容盒子的可见区域高度
this.scrollTop = document.querySelector("#box").offsetHeight;
setTimeout(() => {
//加个定时器,防止上面高度没获取到,再获取一遍。
if (this.scrollTop != this.$refs.scrollBox.offsetHeight) {
this.scrollTop = document.querySelector("#box").offsetHeight;
}
}, 100);
//scrollTop:滚动条距离顶部的距离。
//把上面获取到的高度座位距离,把滚动条顶到最底部
this.$refs.scrollBox.scrollTop = this.scrollTop;
//判断是否有滚动条,有滚动条就创建一个监听滚动事件,滚动到顶部触发srTop方法
this.$refs.scrollBox.addEventListener("scroll", this.srTop);
},
//滚动条到达顶部
srTop() {
//判断:当滚动条距离顶部为0时代表滚动到顶部了
if (this.$refs.scrollBox.scrollTop == 0 && this.abcd == 0) {
this.page = this.page + 1
//获取下一页聊天数据
this.getPriChatmessage()
}
},
//处理时间函数
dealDate(time) {
let d = time ? new Date(time) : new Date();
let year = d.getFullYear();
let month = d.getMonth() + 1;
let day = d.getDate();
let hours = d.getHours();
let min = d.getMinutes();
let seconds = d.getSeconds();
if (month < 10) month = '0' + month;
if (day < 10) day = '0' + day;
if (hours < 0) hours = '0' + hours;
if (min < 10) min = '0' + min;
if (seconds < 10) seconds = '0' + seconds;
return (year + '/' + month + '/' + day + ' ' + hours + ':' + min);
}
},
};
</script>
3:总结
上述代码中注释部分已经将前端的代码逻辑讲的大概了,其中后端接口和后端字段数据,我这是从后端同事取得。应当选择适合自己的方法和逻辑。
2023/10/23,记录