效果显示
WebSocket连接
使用全局变量
本小程序在用户浏览首页的时候创建WebSocket连接,并将连接获得的WebSocket对象存储到全局变量中,方便其他页面来使用WebSocket
首先在项目的main.js文件中声明全局变量socket
Vue.prototype.$socket = null
对全局变量进行赋值
Vue.prototype.$socket = this.$socket;
后续如果需要使用全局变量,直接使用this.$socket
即可
WebSocket连接细节
下面的代码中有一个headbeat方法,该方法主要用来定时给WebSocket服务器发送一个信号,告诉WebSocket服务器当前客户端还处于连接状态。当心跳停止的时候(比如客户端断网),后端服务就会将用户信息从连接中移除
/** * 创建websocket连接 */ initWebsocket() { // console.log("this.socket:" + JSON.stringify(this.$socket)) // this.$socket == null,刚刚进入首页,还没有建立过websocket连接 // this.$socket.readyState==0 表示正在连接当中 // this.$socket.readyState==1 表示处于连接状态 // this.$socket.readyState==2 表示连接正在关闭 // this.$socket.readyState==3 表示连接已经关闭 if (this.$socket == null || (this.$socket.readyState != 1 && this.$socket.readyState != 0)) { this.$socket = uni.connectSocket({ url: "ws://10.23.17.146:8085/websocket/" + uni.getStorageSync("curUser").userName, success(res) { console.log('WebSocket连接成功', res); }, }) // console.log("this.socket:" + this.$socket) // 监听WebSocket连接打开事件 this.$socket.onOpen((res) => { console.log("websocket连接成功") Vue.prototype.$socket = this.$socket; // 连接成功,开启心跳 this.headbeat(); }); // 连接异常 this.$socket.onError((res) => { console.log("websocket连接出现异常"); // 重连 this.reconnect(); }) // 连接断开 this.$socket.onClose((res) => { console.log("websocket连接关闭"); // 重连 this.reconnect(); }) } }, /** * 重新连接 */ reconnect() { console.log("重连"); // 防止重复连接 if (this.lockReconnect == true) { return; } // 锁定,防止重复连接 this.lockReconnect = true; // 间隔一秒再重连,避免后台服务出错时,客户端连接太频繁 setTimeout(() => { this.initWebsocket(); }, 1000) // 连接完成,设置为false this.lockReconnect = false; }, // 开启心跳 headbeat() { console.log("websocket心跳"); var that = this; setTimeout(function() { if (that.$socket.readyState == 1) { // websocket已经连接成功 that.$socket.send({ data: JSON.stringify({ status: "ping" }) }) // 调用启动下一轮的心跳 that.headbeat(); } else { // websocket还没有连接成功,重连 that.reconnect(); } }, that.heartbeatTime); },
最近和自己聊天的用户信息
界面效果
界面代码
<template> <view class="container"> <scroll-view @scrolltolower="getMoreChatUserVo"> <view v-for="(chatUserVo,index) in chatUserVoList" :key="index" @click="trunToChat(chatUserVo)"> <view style="height: 10px;"></view> <view class="chatUserVoItem"> <view style="display: flex;align-items: center;"> <uni-badge class="uni-badge-left-margin" :text="chatUserVo.unReadChatNum" absolute="rightTop" size="small"> <u--image :showLoading="true" :src="chatUserVo.userAvatar" width="50px" height="50px" :fade="true" duration="450"> <view slot="error" style="font-size: 24rpx;">加载失败</view> </u--image> </uni-badge> </view> <view style="margin: 10rpx;"></view> <view style="line-height: 20px;width: 100%;display: flex;justify-content: space-between;flex-direction: column;"> <view style="display: flex; justify-content: space-between;"> <view> <view class="nickname">{{chatUserVo.userNickname}} </view> <view class="content">{{chatUserVo.lastChatContent}}</view> </view> <view class="date">{{formatDateToString(chatUserVo.lastChatDate)}}</view> </view> <!-- <view style="height: 10px;"></view> --> <u-line></u-line> </view> </view> </view> </scroll-view> </view> </template> <script> import { listChatUserVo } from "@/api/market/chat.js"; import { listChat } from "@/api/market/chat.js" export default { data() { return { chatUserVoList: [], page: { pageNum: 1, pageSize: 15 }, } }, created() { }, methods: { /** * 滑动到底部,自动加载新一页的数据 */ getMoreChatUserVo() { this.page.pageNum++; this.listChatUserVo(); }, listChatUserVo() { listChatUserVo(this.page).then(res => { // console.log("res:"+JSON.stringify(res.rows)) // this.chatUserVoList = res.rows; for (var i = 0; i < res.rows.length; i++) { this.chatUserVoList.push(res.rows[i]); } }) }, /** * 格式化日期 * @param {Object} date */ formatDateToString(dateStr) { let date = new Date(dateStr); // 今天的日期 let curDate = new Date(); if (date.getFullYear() == curDate.getFullYear() && date.getMonth() == curDate.getMonth() && date .getDate() == curDate.getDate()) { // 如果和今天的年月日都一样,那就只显示时间 return this.toDoubleNum(date.getHours()) + ":" + this.toDoubleNum(date.getMinutes()); } else { // 如果年份一样,就只显示月日 return (curDate.getFullYear() == date.getFullYear() ? "" : (date.getFullYear() + "-")) + this .toDoubleNum(( date .getMonth() + 1)) + "-" + this.toDoubleNum(date.getDate()); } }, /** * 如果传入的数字是两位数,直接返回; * 否则前面拼接一个0 * @param {Object} num */ toDoubleNum(num) { if (num >= 10) { return num; } else { return "0" + num; } }, /** * 转到私聊页面 */ trunToChat(chatUserVo) { let you = { avatar: chatUserVo.userAvatar, nickname: chatUserVo.userNickname, username: chatUserVo.userName } uni.navigateTo({ url: "/pages/chat/chat?you=" + encodeURIComponent(JSON.stringify(you)) }) }, /** * 接收消息 */ receiveMessage() { this.$socket.onMessage((response) => { // console.log("接收消息:" + response.data); let message = JSON.parse(response.data); // 收到消息,将未读消息数量加一 for (var i = 0; i < this.chatUserVoList.length; i++) { if (this.chatUserVoList[i].userName == message.from) { this.chatUserVoList[i].unReadChatNum++; // 显示对方发送的最新消息 listChat(message.from, { pageNum: 1, pageSize: 1 }).then(res => { this.chatUserVoList[i].lastChatContent = res.rows[0].content }); break; } } }) }, }, onLoad(e) { this.receiveMessage(); }, onShow: function() { this.chatUserVoList = []; this.listChatUserVo(); }, } </script> <style lang="scss"> .container { padding: 20rpx; .chatUserVoItem { display: flex; margin: 0 5px; .nickname { font-weight: 700; } .content { color: #A7A7A7; font-size: 14px; /* 让消息只显示1行,超出的文字内容使用...来代替 */ overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; } .date { color: #A7A7A7; font-size: 12px; } } // .uni-badge-left-margin { // margin-left: 10px; // } } </style>
最近的聊天内容太长
当最近的一条聊天内容太长的时候,页面不太美观,缺少整齐的感觉
解决的方式非常简单,只需要添加以下样式即可
.content { /* 让消息只显示1行,超出的文字内容使用...来代替 */ overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; }
日期时间显示
本文显示日期时间的时候,遵循以下原则:
- 如果上次聊天时间的年月日和今天一致,那就只显示时间,即显示
时:分
- 如果上次聊天时间的年份和今年一致,那就只显示
月-日
- 如果上面的条件都不满足,就显示
年-月-日
在显示月、日、时、分的时候,如果数字是一位数字
,就在前面补一个零,具体操作如方法toDoubleNum
/** * 格式化日期 * @param {Object} date */ formatDateToString(dateStr) { let date = new Date(dateStr); // 今天的日期 let curDate = new Date(); if (date.getFullYear() == curDate.getFullYear() && date.getMonth() == curDate.getMonth() && date .getDate() == curDate.getDate()) { // 如果和今天的年月日都一样,那就只显示时间 return this.toDoubleNum(date.getHours()) + ":" + this.toDoubleNum(date.getMinutes()); } else { // 如果年份一样,就只显示月日 return (curDate.getFullYear() == date.getFullYear() ? "" : (date.getFullYear() + "-")) + this .toDoubleNum(( date .getMonth() + 1)) + "-" + this.toDoubleNum(date.getDate()); } }, /** * 如果传入的数字是两位数,直接返回; * 否则前面拼接一个0 * @param {Object} num */ toDoubleNum(num) { if (num >= 10) { return num; } else { return "0" + num; } },
未读消息数量显示
未读消息数量显示使用角标组件,即uni-badge
,使用该组件需要下载安装插件,下载链接,下载之前需要看广告,哈哈哈,当然有钱可以不看
显示效果如下图
<uni-badge class="uni-badge-left-margin" :text="chatUserVo.unReadChatNum" absolute="rightTop" size="small"> <u--image :showLoading="true" :src="chatUserVo.userAvatar" width="50px" height="50px" :fade="true" duration="450"> <view slot="error" style="font-size: 24rpx;">加载失败</view> </u--image> </uni-badge>
私聊界面
界面展示
【微信公众平台模拟的手机界面】
【手机端,键盘呼出之后的聊天区域】
代码实现
<template> <view style="height:100vh;"> <!-- @scrolltoupper:上滑到顶部执行事件,此处用来加载历史消息 --> <!-- scroll-with-animation="true" 设置滚动条位置的时候使用动画过渡,让动作更加自然 --> <scroll-view :scroll-into-view="scrollToView" scroll-y="true" class="messageListScrollView" :style="{height:scrollViewHeight}" @scrolltoupper="getHistoryChat()" :scroll-with-animation="!isFirstListChat" ref="chatScrollView"> <view v-for="(message,index) in messageList" :key="message.id" :id="`message`+message.id" style="width: 750rpx;min-height: 60px;"> <view style="height: 10px;"></view> <view v-if="message.type==0" class="messageItemLeft"> <view style="width: 8px;"></view> <u--image :showLoading="true" :src="you.avatar" width="50px" height="50px" radius="3"></u--image> <view style="width: 7px;"></view> <view class="messageContent left"> {{message.content}} </view> </view> <view v-if="message.type==1" class="messageItemRight"> <view class="messageContent right"> {{message.content}} </view> <view style="width: 7px;"></view> <u--image :showLoading="true" :src="me.avatar" width="50px" height="50px" radius="3"></u--image> <view style="width: 8px;"></view> </view> </view> </scroll-view> <view class="messageSend"> <view class="messageInput"> <u--textarea v-model="messageInput" placeholder="请输入消息内容" autoHeight> </u--textarea> </view> <view style="width:5px"></view> <view class="commmitButton" @click="send()">发 送</view> </view> </view> </template> <script> import { getUserProfileVo } from "@/api/user"; import { listChat } from "@/api/market/chat.js" let socket; export default { data() { return { webSocketUrl: "", socket: null, messageInput: '', // 我自己的信息 me: {}, // 对方信息 you: {}, scrollViewHeight: undefined, messageList: [], // 底部滑动到哪里 scrollToView: '', page: { pageNum: 1, pageSize: 15 }, isFirstListChat: true, loadHistory: false, // 消息总条数 total: 0, } }, created() { this.me = uni.getStorageSync("curUser"); }, beforeDestroy() { console.log("执行销毁方法"); this.endChat(); }, onLoad(e) { // 设置初始高度 this.scrollViewHeight = `calc(100vh - 20px - 44px)`; this.you = JSON.parse(decodeURIComponent(e.you)); uni.setNavigationBarTitle({ title: this.you.nickname, }) this.startChat(); this.listChat(); this.receiveMessage(); }, onReady() { // 监听键盘高度变化,以便设置输入框的高度 uni.onKeyboardHeightChange(res => { let keyBoardHeight = res.height; console.log("keyBoardHeight:" + keyBoardHeight); this.scrollViewHeight = `calc(100vh - 20px - 44px - ${keyBoardHeight}px)`; this.scrollToView = ''; setTimeout(() => { this.scrollToView = 'message' + this.messageList[this .messageList.length - 1].id; }, 150) }) }, methods: { /** * 发送消息 */ send() { if (this.messageInput != '') { let message = { from: this.me.userName, to: this.you.username, text: this.messageInput } // console.log("this.socket.send:" + this.$socket) // 将组装好的json发送给服务端,由服务端进行转发 this.$socket.send({ data: JSON.stringify(message) }); this.total++; let newMessage = { // code: this.messageList.length, type: 1, content: this.messageInput }; this.messageList.push(newMessage); this.messageInput = ''; this.toBottom(); } }, /** * 开始聊天 */ startChat() { let message = { from: this.me.userName, to: this.you.username, text: "", status: "start" } // 告诉服务端要开始聊天了 this.$socket.send({ data: JSON.stringify(message) }); }, /** * 结束聊天 */ endChat() { let message = { from: this.me.userName, to: this.you.username, text: "", status: "end" } // 告诉服务端要结束聊天了 this.$socket.send({ data: JSON.stringify(message) }); }, /** * 接收消息 */ receiveMessage() { this.$socket.onMessage((response) => { // console.log("接收消息:" + response.data); let message = JSON.parse(response.data); let newMessage = { // code: this.messageList.length, type: 0, content: message.text }; this.messageList.push(newMessage); this.total++; // 让scroll-view自动滚动到最新的数据那里 // this.$nextTick(() => { // // 滑动到聊天区域最底部 // this.scrollToView = 'message' + this.messageList[this // .messageList.length - 1].id; // }); this.toBottom(); }) }, /** * 查询对方和自己最近的聊天数据 */ listChat() { return new Promise((resolve, reject) => { listChat(this.you.username, this.page).then(res => { for (var i = 0; i < res.rows.length; i++) { this.total = res.total; if (res.rows[i].fromWho == this.me.userName) { res.rows[i].type = 1; } else { res.rows[i].type = 0; } // 将消息放到数组的首位 this.messageList.unshift(res.rows[i]); } if (this.isFirstListChat == true) { // this.$nextTick(function() { // // 滑动到聊天区域最底部 // this.scrollToView = 'message' + this.messageList[this // .messageList.length - 1].id; // }) this.toBottom(); this.isFirstListChat = false; } resolve(); }) }) }, /** * 滑到最顶端,分页加一,拉取更早的数据 */ getHistoryChat() { // console.log("获取历史消息") this.loadHistory = true; if (this.messageList.length < this.total) { // 当目前的消息条数小于消息总量的时候,才去查历史消息 this.page.pageNum++; this.listChat().then(() => {}) } }, /** * 滑动到聊天区域最底部 */ toBottom() { // 让scroll-view自动滚动到最新的数据那里 this.scrollToView = ''; setTimeout(() => { // 滑动到聊天区域最底部 this.scrollToView = 'message' + this.messageList[this .messageList.length - 1].id; }, 150) } } } </script> <style lang="scss"> .messageListScrollView { background: #F5F5F5; overflow: auto; .messageItemLeft { display: flex; align-items: flex-start; justify-content: flex-start; .messageContent { max-width: calc(750rpx - 10px - 50px - 15px - 10px - 50px - 15px); padding: 10px; // margin-top: 10px; border-radius: 7px; font-family: sans-serif; // padding: 10px; // 让view只包裹文字 width: auto; // display: inline-block !important; // display: inline; // 解决英文字符串、数字不换行的问题 word-break: break-all; word-wrap: break-word; } } .messageItemRight { display: flex; align-items: flex-start; justify-content: flex-end; .messageContent { max-width: calc(750rpx - 10px - 50px - 15px - 10px - 50px - 15px); padding: 10px; // margin-top: 10px; border-radius: 7px; font-family: sans-serif; // padding: 10px; // 让view只包裹文字 width: auto; // display: inline-block !important; // display: inline; // 解决长英文字符串、数字不换行的问题 word-wrap: break-word; } } .right { background-color: #94EA68; } .left { background-color: #ffffff; } } .messageSend { display: flex; background: #ffffff; padding-top: 5px; padding-bottom: 15px; .messageInput { border: 1px #EBEDF0 solid; border-radius: 5px; width: calc(750rpx - 65px); margin-left: 5px; } .commmitButton { height: 38px; border-radius: 5px; width: 50px; display: flex; align-items: center; justify-content: center; color: #ffffff; background: #3C9CFF; } } </style>
英文长串不换行问题
这个问题属于是整串英文被以为是一个单词了,所以没有换行,看下面的句子,英文单词可以比较短的,所以会自动换行
解决这个问题只需要添加下面的css即可
// 解决长英文字符串、数字不换行的问题 word-wrap: break-word;
下面是添加之后的效果
聊天区域自动滑动到底部
在聊天的时候,无论是发送一条新的消息,还是接收到一条新的消息,聊天区域都需要自动滑动到最新的消息那里。本文使用scroll-view组件来包裹显示聊天消息,在scroll-view组件中,可以通过给scroll-into-view属性赋值来指定聊天区域所显示到的位置。使用时需要注意如下问题:
- 需要先给每一条消息设置一个id属性,id属性存储的内容不能以数字开头,因此本文在id之间拼接了一个字符串’message’
- scroll-view需要被设置好高度,本文通过绑定一个变量来设置高度,如
:style="{height:scrollViewHeight}"
,因为手机端使用小程序打字时键盘呼出会影响聊天区域的高度
后续通过给scrollToView设置不同的值即可控制聊天区域的滑动,比如每接收到一条新的消息,就调用toBottom
方法,该方法通过设置scrollToView为'message' + this.messageList[this.messageList.lengthh - 1].id将聊天区域滑动到最新的消息处。需要注意的是,在进行该值的设置之前,需要延迟一段时间,否则滑动可能不成功,本文延迟150ms,读者也可以探索不同的值,该值不能太大或者太小。
通过设置scroll-view的属性scroll-with-animation的值为true,可以让消息区域在滑动的时候使用动画过渡,这样滑动更加自然。
键盘呼出,聊天区域收缩,聊天区域滑动到底部
当键盘呼出时,需要将聊天区域的高度减去键盘的高度。同时将scrollToView赋值为最后一条消息的id。需要注意的是,在设置scrollToView之前,需要先将scrollToView设置为空字符串,否则滑动效果可能不成功
onReady() { // 监听键盘高度变化,以便设置输入框的高度 uni.onKeyboardHeightChange(res => { let keyBoardHeight = res.height; console.log("keyBoardHeight:" + keyBoardHeight); this.scrollViewHeight = `calc(100vh - 20px - 44px - ${keyBoardHeight}px)`; this.scrollToView = ''; setTimeout(() => { this.scrollToView = 'message' + this.messageList[this .messageList.length - 1].id; }, 150) }) },
通知WebSocket服务器哪两个用户开始聊天
为了便于后端在存储聊天数据的时候辨别消息是否为已读状态。比如,在小王开始聊天之前,需要先告诉后端:“小王要开始和小明聊天了”,如果正好小明也告诉后端:“我要和小王聊天了”,那小王发出去的消息就会被设置为已读状态,因为他们两个此时此刻正在同时和对方聊天,那小王发出去的消息就默认被小明看到了,因此设置为已读状态
/** * 开始聊天 */ startChat() { let message = { from: this.me.userName, to: this.you.username, text: "", status: "start" } // 告诉服务端要开始聊天了 this.$socket.send({ data: JSON.stringify(message) }); }, /** * 结束聊天 */ endChat() { let message = { from: this.me.userName, to: this.you.username, text: "", status: "end" } // 告诉服务端要结束聊天了 this.$socket.send({ data: JSON.stringify(message) }); },