前言

这几天在做的一个需求,就是要写一个小程序端的页面,用于跟客服聊天。
然后就用到了websocket技术,以前我做过网页版的,但是做小程序后发现网页版的逻辑放过来没问题,但是很多的方法和api是不生效的,所以又查文档专门看了一下对于的api,踩了一些坑后来记录一下,方便下次复制!

pc端聊天功能模板(自带websocket,复制可直接用)

点击这里跳转
效果图

功能简介

1,页面信息展示,根据后台返回字段,分左边和右边展示
2,输入后点击发送按钮,可以发送消息,通过websocket传给后台
3,进入页面后链接websocket
4,websocket自带心跳重连设置,如果异常或者断开会一直重连,直到连上为止
5,离开页面时可以断开连接,并终止重连请求
6,登录和离线会给后端发送type为online的信息,0代表离线,1代表在线
7,页面滚动事件监听,当滚动条滚动到顶部时会触发方法
8,历史数据功能,进入页面后拉取历史数据
9,发送信息时滚动条永远保持在最底部

效果图

这是链接后台websocket,聊天的页面样式

后端历史记录的数据结构,分左右

代码(整体代码放上)

放下翻,有写注意事项和逻辑,里面详细解释了一下这个代码的功能实现逻辑和注意项

<template><view><view class="wrap"><view class="title"><view>{{ userName }}</view></view><view class="content_box" id="box" ref="scrollBox"><view class="timer">2022-08-02 11:08:07</view><view :class="item.position == 'left' ? 'userbox2' : 'userbox'" v-for="(item, index) in chatList":key="index" :id='"item"+index'><view :class="item.position == 'left' ? 'nameInfo2' : 'nameInfo'"><view style="font-size: 14px">{{ item.position == 'left' ?item.uname:item.to_name  }}</view><view :class="item.position == 'left' ? 'contentText2' : 'contentText'">{{ item.msn }}</view></view><view><image class="touxiang" :src="item.position == 'left' ?item.uavatar:item.to_avatar" /></view></view></view><view class="bottom"><textarea name="输入框" id="1" cols="20" rows="5" class="areaBox" v-model="inputValue"></textarea><button style="height: 30px;color:#58df4d;font-size: 14px;line-height: 30px;"@click="sendOut">发送</button></view></view></view>
</template><script>export default {data() {return {page: 1,//聊天历史记录分页chatList: [],//聊天信息userName: "",//用户名inputValue: "",//输入内容scrollTop: 0,//滚动条距离顶部距离infoList: null,//用户信息path: "wss://test.jskwsx.com/msg", //websocket链接地址ws: null, //建立的连接lockReconnect: false, //是否真正建立连接timeout: 10 * 1000, //30秒一次心跳timeoutObj: null, //心跳心跳倒计时serverTimeoutObj: null, //心跳倒计时timeoutnum: null, //断开 重连倒计时closeType:1,//断开判断:0代表不重连,1代表重连}},onShow() {this.initWebpack();//初始化this.closeType=1//进入改为1,代表如果断开链接自动重连this.getlishiList()//历史记录this.userName=uni.getStorageSync("userinfo").nickname//拿到缓存中的用户信息},onLoad(options) {this.infoList = JSON.parse(options.urlee)//拿到上一个页面传过来的参数,内容是选中客服信息console.log('选中客服信息', this.infoList);},onPageScroll(e) {//监听滚动事件,如果滚动条等于0,代表滚动到最顶部,把分页加一,然后历史记录拉第二页数据,以此类推if (e.scrollTop == 0) {this.page++this.getlishiList(1)console.log('到顶部了')}},beforeDestroy() {this.closeType=0 //离开页面前改为0,代表离开后断开链接不再重连this.ws.send({data: JSON.stringify({type: "online",data: {online: 0,user_type: 'user',is_tourist: uni.getStorageSync("userinfo").id?0:1}})})// 离开页面后关闭连接this.ws.close();// 清除时间clearTimeout(this.timeoutObj);clearTimeout(this.serverTimeoutObj);},methods: {//获取历史记录getlishiList(type) {uni.request({url: 'https://zz.api.asdwqs.com/gzh/crmebchat/chatMessageList', //仅为示例,并非真实接口地址。method: 'POST',data: {accept_id: this.infoList.kf_id,page: this.page,limit: 10,},header: {token: uni.getStorageSync('token') //拿到缓存中的token},success: (res) => {console.log('历史记录:', res);let a = res.data.data.listthis.chatList = a.concat(this.chatList)//用拿到的数据合并现有的数据,这样当加载第二页历史记录时,顺序不会乱if (type == 1) {//滚动到顶部触发方法会传入1,此时不需要调用滚动到最底部的方法return}this.setPageScrollTo()//滚动到最底部}});},//滚动条默认滚动到最底部setPageScrollTo(s, c) {let that = thisthis.$nextTick(() => {const query = uni.createSelectorQuery().in(this);query.select("#box").boundingClientRect((rect) => {let height = rect.height;//拿到聊天框的高度console.log("聊天信息框高度: ", height);wx.pageScrollTo({scrollTop: height,//把距离顶部距离设置成聊天框高度,以此把滚动条顶到最底部duration: 100 // 滑动速度})}).exec();});},//发送消息sendOut() {this.chatList.push({msn: this.inputValue,position: "right",to_avatar: uni.getStorageSync("userinfo").avatar,to_name: uni.getStorageSync("userinfo").nickname})let parms = {content: this.inputValue,uid: uni.getStorageSync("userinfo").id,uname: uni.getStorageSync("userinfo").nickname,uavatar: uni.getStorageSync("userinfo").avatar,to_uid: this.infoList.kf_id,to_name: this.infoList.kfname,to_avatar: this.infoList.kf_avatar,type: 'text',channel_type: 'wechat',}//通过websocket发送信息到后台this.ws.send({data: JSON.stringify({type: "chat",data: parms})})this.inputValue = ''//点击发送后清空输入框this.setPageScrollTo()//滚动到最底部console.log('发送成功', this.inputValue);},// 初始化websocket链接initWebpack() {//实例this.ws = wx.connectSocket({url: this.path})//链接成功this.ws.onOpen((res) => {let that = thisconsole.log("连接成功", that.ws.readyState);if (that.ws.readyState == 1) {wx.sendSocketMessage({ //发送消息到后台,和send一样,这是微信的写法data: JSON.stringify({type: "login",data: {id: uni.getStorageSync("userinfo").wechatUsers.id,channel_type: 'wechat',uid: uni.getStorageSync("userinfo").id,openid: 'ojV4k6tnkv4_F1dddc3VwLeJ_QLs'}})})this.ws.send({data: JSON.stringify({type: "online",data: {online: 1,user_type: 'user',is_tourist: uni.getStorageSync("userinfo").id?0:1}})})}that.start(); //链接成功后开启心跳})//链接异常this.ws.onError((res) => {console.log("出现错误");this.reconnect(); //重连})//链接断开this.ws.onClose((res) => {console.log("连接关闭");//断开链接时判断if(this.closeType==0){return}this.reconnect(); //重连})//后台返回消息this.ws.onMessage((res) => {let type = JSON.parse(res.data)//后台返回消息,通过type字段判断是不是别人发送给我的消息if (type.type == 'chat') {this.chatList.push(type.data)//把消息添加到信息列表渲染this.setPageScrollTo() //滚动到最底部console.log("收到后台信息:", JSON.parse(res.data));}this.reset(); //收到服务器信息,心跳重置})},//重新连接reconnect() {var that = this;//防止重复链接if (that.lockReconnect) {return;}that.lockReconnect = true;//没连接上会一直重连,设置延迟避免请求过多that.timeoutnum && clearTimeout(that.timeoutnum);that.timeoutnum = setTimeout(function() {that.initWebpack(); //新连接that.lockReconnect = false;}, 5000);},//重置心跳reset() {var that = this;clearTimeout(that.timeoutObj); //清除心跳倒计时clearTimeout(that.serverTimeoutObj); //清除超时关闭倒计时that.start(); //重启心跳},//开启心跳start() {var self = this;self.timeoutObj && clearTimeout(self.timeoutObj); //心跳倒计时如果有值就清除掉,防止重复self.serverTimeoutObj && clearTimeout(self.serverTimeoutObj); //超时关闭倒计时如果有值就清除掉,防止重复self.timeoutObj = setTimeout(function() {if (self.ws.readyState == 1) {wx.sendSocketMessage({data: JSON.stringify({type: "ping"})})} else {self.reconnect();//重连}//如果超时了就关闭连接self.serverTimeoutObj = setTimeout(function() {self.ws.close();}, self.timeout);}, self.timeout);},//连接成功}}
</script><style scoped>.wrap {height: 100%;width: 100%;position: relative;}.touxiang {width: 50px;height: 50px;border-radius: 50%;}.areaBox {height: 40px;}.title {height: 40px;width: 100%;background-color: #eaeaea;display: flex;justify-content: center;align-items: center;}.bottom {min-height: 50px;width: 100%;border-top: 1px solid #eaeaea;background-color: #F1F1F1;position: fixed;bottom: 0;display: flex;justify-content: space-between;align-items: center;padding: 0 5px;border-radius: 10px;}.content_box {/* 中间栏计算高度,110是包含了上下固定的两个元素高度90这里padding:10px造成的上下够加了10,把盒子撑大了,所以一共是20要减掉然后不知道是边框还是组件的原因,导致多出了一些,这里再减去5px刚好。不然会出现滚动条到顶或者底部的时候再滚动的话就会报一个错,或者出现滚动条变长一下的bug*/height: calc(100% - 115px);overflow: auto;padding: 10px 10px 50px 10px;}.timer {text-align: center;color: #c2c2c2;}/* 发送的信息样式 *//*
右边消息思路解释:首先大盒子userbox内放两个盒子,一个放头像,一个放用户名和发送的内容,我们先用flex让他横向排列。
然后把写文字的大盒子设置flex:1。这个属性的意思就是让这个元素撑满父盒子剩余位置。然后我们再把文字盒子设置flex,并把他对齐方式设置为尾部对齐就完成了基本的结构,然后微调一下就可以了
*/.userbox {width: 100%;display: flex;margin-bottom: 10px;}.nameInfo {/* 用flex:1把盒子撑开 */flex: 1;margin-right: 10px;/* 用align-items把元素靠右对齐 */display: flex;flex-direction: column;align-items: flex-end;}.contentText {background-color: #9eea6a;/* 把内容部分改为行内块元素,因为盒子flex:1把盒子撑大了,所以用行内块元素让内容宽度不根据父盒子来 */display: inline-block;/* 这四句是圆角 */border-top-left-radius: 10px;border-top-right-radius: 0px;border-bottom-right-radius: 10px;border-bottom-left-radius: 10px;/* 最大宽度限定内容输入到百分61换行 */max-width: 61%;padding: 5px 10px;/* 忽略多余的空白,只保留一个空白 */white-space: normal;/* 换行显示全部字符 */word-break: break-all;margin-top: 3px;font-size: 14px;}/* 接收的信息样式 *//*
左边消息思路解释:跟上面一样,就是换一下位置,首先通过把最外层大盒子的排列方式通过flex-direction: row-reverse;属性翻转,也就是头像和文字盒子换位置
然后删除掉尾部对齐方式,因为不写这个默认是左对齐的。我们写的左边就没必要再写了。
*/.userbox2 {width: 100%;display: flex;flex-direction: row-reverse;margin-bottom: 10px;}.nameInfo2 {/* 用flex:1把盒子撑开 */flex: 1;margin-left: 10px;}.contentText2 {background-color: #9eea6a;/* 把内容部分改为行内块元素,因为盒子flex:1把盒子撑大了,所以用行内块元素让内容宽度不根据父盒子来 */display: inline-block;/* 这四句是圆角 */border-top-left-radius: 0px;border-top-right-radius: 10px;border-bottom-right-radius: 10px;border-bottom-left-radius: 10px;/* 最大宽度限定内容输入到百分61换行 */max-width: 61%;padding: 5px 10px;/* 忽略多余的空白,只保留一个空白 */white-space: normal;/* 换行显示全部字符 */word-break: break-all;margin-top: 3px;font-size: 14px;}
</style>

注意点

1,点击发送时,我们看到的并不是后台传过来的信息,是我们自己push进去的,然后信息通过websockt传给后台了,这样就不需要再获取一次消息了。
2,我这里后台的数据结构时分左右的,然后左右的不同也会有不同的字段,所以我标签上有判断,如果你们后端不这么传,自行更改
3,这里用css保留了空格等格式,如果不生效,可以直接用vue自带的v-html来把文字放进去

聊天功能逻辑简介

简单来说就是,我们前端的页面渲染是通过写两个style样式。一个是靠左边,一个靠右边。然后通过后台的数据内有一个position字段,是left我们就调用左边的样式,是right的就调用右边的样式。这样,页面就出来了。
然后我们通过websocket链接交互。
逻辑是,刚连接上时发送给后台当前用户的信息(type=login)和这个用户上线了的状态(type=online),然后发送消息的时候type=chat,并带上参数。参数为我的信息和我要发送的人的信息。然后接收的时候我们通过判断后台websocket传过来的数据内type等于什么来处理不同的数据。这一点需要你自行和后端商量什么type。然后离开页面的时候也发送一下type=online的消息,表示用户离线了。然后关闭链接

拓展项

【用户登录状态,在线离线】
这里我写了一个pc版的在线和离线。代码放上来讲解一下。
这里是截取的代码,是用来渲染用户列表的,是pc端截取出来的。大家看逻辑就行,这里用户列表自带状态显示上下线,所以我分了两个盒子来判断。
逻辑很简单,就是两个盒子,先判断当前这个用户的id和后台websocket发来的id是否一致。如果不一致,那么我们使用上面的盒子,也就是用户列表内自带的字段来判断上下线。
如果后台发了一个上下线的状态过来,我们就赋值给online_uid。这时候再判断,就会发现用户列表内某一个用户id是一致的,这时候这个用户就会使用下面的盒子,判断就变成了后台发送过来的状态来判断上下线了。

html

<!-- 用户状态判断:列表拉状态时,判断每一个用户id和ws推送的id是否一致,不一致的默认显示列表内的状态 -->
<div v-show="item.user_id!==online_uid"><div class="statebox" v-if="item.online_user==1"> 在线</div><div class="statebox2" v-else> 离线</div>
</div>
<!-- 如果判断该用户id和ws传的uid一致,那么使用ws推送的状态 -->
<div v-show="item.user_id==online_uid"><div class="statebox" v-show="online_type==1&&item.user_id==online_uid"> 在线</div><div class="statebox2" v-show="online_type==0&&item.user_id==online_uid"> 离线</div>
</div>

data

data() {return {online_type:0,//0是离线,1是在线online_uid:0,//用于判断uid是否一致};},

methods

            //接受后台信息回调onmessage(e) {let type = JSON.parse(e.data)if (type.type == 'reply') {//reply代表用户发送的消息this.userInfoList.push(type.data)//把发送的消息添加进信息列表渲染}else if(type.type == 'online'){//online代表用户上下线this.online_type=type.data.online//用户状态this.online_uid=type.data.uid//用户id}console.log("收到后台信息:", JSON.parse(e.data));},

【发送图片,视频,商品详情等信息】
这里是很多聊天功能会拓展的部分,不可能只发送文字,目前我还没有写代码,先记录逻辑。

还记得我们上面代码里面发送消息的时候是有type=text这个属性吗,我们就通过这个type来区分。
比如我点击上传图片时,方法内给type改成img。那么后端发给我们,也就是img类型的,我们通过这个判断来完成功能,简单描述一下

消息分类:
1,text:文字信息
2,img:图片信息
3,shop:购物信息
4,video:视频信息
逻辑:position分左右不用动,创建四个div。用v-if来判断type类型属于哪一个就显示哪个div。同时,每个div内对应获取信息列表中该对象的一些字段渲染出来,比如type等于text就代表是文字信息,拿msn字段的文字去渲染。如果type等于img。就是图片信息,图片的这个div内获取url字段的图片地址去渲染。如果是shop字段。那么就对应获取比如名称,描述,价格,图片等字段渲染出来一个小卡片形式的结构。

【小程序websocket前后端交互】uniapp写微信小程序聊天功能功能,websocket交互功能,心跳重连【详细注释,复制即用】相关推荐

  1. 前后端分离的Java微信小程序B2C商城 H5+APP源码

    Java B2C商城微信小程序 H5+APP源码 前后端分离 H5+微信小程序+ Android+IOS, Java SpringBoot+vue 开发语言:JAVA 数据库:MySQL 开发工具:E ...

  2. uniapp写微信小程序怎么运行到微信开发工具上

    1.选择运行>运行到小程序模拟器>运行到微信开发者工具 2.这样unpackage中就多一个文件mp-weixin 3.把这个文件导入到微信小程序中就行啦

  3. Ruoyi-vue前后端不分离集成微信小程序授权登录思路

    背景 公司需要开发一个小程序,后台使用的是ruoyi-vue框架,前端使用uiapp开发.小程序登录采用手机号验证码登录. 思路 1.重新改写登录逻辑 ruoyi-vue采用的是spring secu ...

  4. uni-app写微信小程序获取位置信息

    1.调用api获取自身经纬度 uni.getLocation({type: 'gcj02',//腾讯地图使用gcj02获取位置坐标success: function (res) {console.lo ...

  5. uniapp与微信小程序的区别

    在微信小程序的平台上,uniapp与微信小程序的基础用法除了语法外基本一致.最近在用uniapp做小程序项目,发现了一些语法区别. 点击事件 微信小程序:bindtap uniapp:@click 函 ...

  6. 【websocket前后端交互】vue-springboot实现websocket前后端交互链接,websocket心跳重连,包含前后端代码,复制即可用【详细解释版本】

    前言: 还是老规矩,一步步的教大家如何建立前后端的 websocket 链接,并能完成互相传送数据的简单功能.由于网上找了半天发现很多帖子都是东一句西一句的,要不就是写的没什么注释和解释,导致我这个前 ...

  7. uniapp手写_【转】uni-app框架纯手写微信小程序开发左侧滑动菜单

    本帖最后由 fengrui99 于 2020-7-22 14:38 编辑 原文来自:在学习的uni-app的微信小程序开发路上慢慢开始不一直依赖插件(但是使用插件是真的香,一直用一直香) 在大佬的指引 ...

  8. 智慧车行预约小程序,汽车保养、维修、美容、检测预测小程序,前后端完整代码包括车行动态,养车常识,保养预约,维修预约,洗车美容预约

    功能介绍 智慧车行小程序,是一个专门为洗车/4S/车辆维修行业打造的小程序,前后端完整代码包括车行动态,养车常识,保养预约,维修预约,洗车美容预约,汽车检测预约等功能,采用腾讯提供的小程序云开发解决方 ...

  9. uniapp手写_uni-app框架纯手写微信小程序开发左侧滑动菜单

    原来到最后才发现有些东西,没有就真的没有.不行,就真的不行 唠叨一会 在学习的uni-app的微信小程序开发路上慢慢开始不一直依赖插件(但是使用插件是真的香,一直用一直香),在大佬的指引下学会自己去写 ...

最新文章

  1. linux的Nginx安装、默认虚拟主机、用户认证、域名重定向配置介绍
  2. 小巧密码破解工具IE PassView使用指南
  3. Java_异常_04_ OutOfMemoryError系列
  4. 【文献阅读】Stacked What-Where Auto-encoders -ICLR-2016
  5. KARL MAYER卡尔迈耶驱动器维修SP0405-KM SP0404 SP0403
  6. python代码补全_python命令自动补全
  7. H无穷控制学习笔记——H无穷/H2控制
  8. 粘贴时word左下角出现“正在与服务器联系以获取信息,按ESC取消”
  9. 计算机组装防静电措施,浅谈组装电脑如何防静电与去除机箱静电的方法
  10. 新东方王强的一篇精彩演讲
  11. [魔方]魔教秘籍4:《封王-易筋经》(概要)
  12. [跨境工具通]2020年Shopify卖家可以免费使用的10款Dropshipping工具推荐
  13. ACT技能编辑器的制作经验
  14. 自动驾驶小实验之Turtlebot3_Autopilot(Tensorflow, OpenCV, ROS, PID)
  15. 新网络时代学习方式的大变革
  16. U9系统报错:Enterprise[9999] 不存在!
  17. 使用JabRef在WORD中自动引用参考文献的方法
  18. 服务器禁用网络协议,启用或禁用服务器网络协议
  19. 阿里否认投资今日头条;小米为上市作人事调整;iPhone X销量大幅下滑丨价值早报...
  20. 15分钟教你推广网站、增加流量

热门文章

  1. 摩托罗拉android产品 MT710
  2. 如何解决ssh登陆,不久会自动断开的问题
  3. oracle ORA-00257异常处理及定时清理归档日志
  4. 华为2288H-V5服务器做raid的详细步骤
  5. 目标检测中NMS的GPU实现(来自于Faster R-CNN中的nms_kernel.cu文件)
  6. Application run failed问题解决
  7. delphi美团点评劵码核销API(支持验劵、 验劵记录查询、撤销验劵)
  8. debian php-fpn_如何在基于 Ubuntu 或 Debian 的 Linux 发行版中查看一个软件包的依赖...
  9. jsp mysql下拉框联动_phpajax下拉框动态联动问题-爱问知识人
  10. 《花雕学AI》14:免费打开就可用,ChatGPT国内12个镜像站盘点与测试