【uni-app】小程序实现微信在线聊天(私聊/群聊)

news2025/2/27 7:18:07

之前学习使用uni-app简单实现一个在线聊天的功能,今天记录一下项目核心功能的实现过程。页面UI以及功能逻辑全部来源于微信,即时聊天业务的实现使用socket.io,前端使用uni-app开发,后端服务器基于node实现,数据库选择mongoDB。

首先在系统中注册两个用户,将对方添加为好友后,开始正常聊天,先简单看一下聊天功能的效果图,分为私聊和群聊两大部分

一对一聊天效果:

在好友列表中添加群成员创建群后即可群聊,群聊效果:

目录

聊天信息列表的渲染

聊天信息发送的相关问题

实现一对一聊天

关于websocket

建立连接

存储连接的用户

发送聊天信息 

首页新消息提示

实现群聊

加入房间

发送群消息


聊天信息列表的渲染

聊天信息列表区域是一个滚动区,这里使用scroll-view组件,其中对于聊天信息展示,主要分为自己的消息和好友的消息,自己的消息位于右侧,好友的消息位于左侧,所以静态页面阶段要实现是左侧消息和右侧消息的页面布局,以及这些消息类型为文字,图片,语音,位置信息时的布局。

后端接口返回的聊天信息是按照时间顺序排列的,渲染聊天信息时使用v-for遍历接口返回的消息列表的内容即可,需要注意的是,还需要使用条件渲染v-if根据每一条消息的发送者id和当前用户的id判断消息的发送方和接受方,渲染在左右指定的区域,当前用户的id从本地存储localStorage中获取;还有就是使用条件渲染判断消息的类型,是文字,图片,语音或定位,合理展示。

<!-- 一条聊天记录  -->
<view class="chat-item" v-for="(item,index) in msg" :key="item.id">
    <!-- 时间  -->
	<view class="time" v-if="item.isShowTime">{{handleTime(item.time)}}</view>
	<!-- b - 对方的消息  -->
	<view class="content-wrapper-left" v-if="item.fromId !== uid" >
        <!-- 头像 -->
		<image :src="item.imgUrl" class="avator avator-left"></image>
		<!-- 0 - 文字 -->
		<view class="chat-content-left" v-if="item.types === '0'">......</view>
		<!-- 1 - 图片 -->
		<view class="chat-image-left" v-if="item.types === '1'">......</view>
		<!-- 2 - 语音 -->
		<view class="chat-voice-left" v-if="item.types === '2'">......</view>
		<!-- 3 - 位置信息 -->
		<view class="chat-site-left" v-if="item.types === '3'">......</view>	
	</view>
	<!--a - 自己的信息-->
	<view class="content-wrapper-right" v-if="item.fromId === uid">
		<!-- 0 - 文字 -->
		<view class="chat-content-right" v-if="item.types === '0'">......</view>
		<!-- 1 - 图片 -->
		<view class="chat-image-right" v-if="item.types === '1'">......</view>
		<!-- 2 - 语音 -->
		<view class="chat-voice-right" v-if="item.types === '2'">......</view>
		<!-- 3 - 位置信息 -->
		<view class="chat-site chat-site-right">......</view>
		<!-- 头像 -->
		<image :src="item.imgUrl" class="avator avator-right"></image>
	</view>
</view>

聊天信息发送的相关问题

点击发送按钮,正式将信息发送给服务器之前,还有几个问题需要解决,这里面有许多坑,在实现的时候走了不少弯路。

1.scroll-view如何始终定位在最底部?

如下图,当发送了一条聊天信息时,聊天信息列表就会增加这条消息,之所以能够看到这条消息,那是因为scroll-view的滚动条在消息添加时将位置定位到了最底部,这是需要进行一些处理的,默认效果是这样的

是不是很变扭?这样的用户体验很差,滚动条不会自动定位到底部,这里需要给scroll-view组件添加一个scroll-into-view属性,按照官方文档的说法它的值应为某子元素id。设置哪个方向可滚动,则在哪个方向滚动到该元素,也就是说可以动态的修改这个属性的值,从而让scroll-view组件的滚动到想要滚动的页面元素位置。

这里就给每一个scroll-view的子元素(聊天记录item)添加id属性,属性值为 msg + 每条聊天记录的id

<scroll-view class="chat-main" 
 scroll-y="true" 
 :scroll-into-view="scrollToView"
 :scroll-with-animation="needScrollAnimation"
 :style="{height:paddingBottom}">
    <!-- 聊天记录item --->
    <view class="chat-item" v-for="(item,index) in msg" :id="'msg' + item.id" :key="item.id" >
        ......
    </view>
</scroll-view>

在发送消息的方法中修改scroll-into-view的值scrollToView,让其为最新一条聊天记录即msg.length - 1的id值,必须使用在$nextTick回调中,这是为了在新的聊天记录渲染完毕后再去定位。

this.$nextTick(function(){
	this.scrollToView = 'msg' + this.msg[this.msg.length - 1].id;
});

 这样才能实现最终的效果

 

2.如何动态修改scroll-view的高度

如下图,点击 + 按钮发送位置信息时会弹出底部菜单栏,但此时scroll-view内的聊天内容会被覆盖,用户想要看最后一条记录还需操作滚动条,这也是不好的用户体验。

需要做到的是弹出底部菜单栏的同时减小聊天内容区域scroll-view组件的高度,让用户能够完整的看到最后的聊天记录。

需要获取底部菜单栏弹出的高度,随后让scroll-view组件减少这部分高度即可。在uni-app中无法操作dom,获取元素的尺寸使用createSelectorQuery获取页面节点,再用 boundingClientRect查询节点的尺寸。官方文档:uni.createSelectorQuery() | uni-app官网

使用如下代码获取页面节点的尺寸,可能无法及时获取到(得到的可能是undefined),这里需要用定时器包裹,才能拿到菜单栏的高度

<view class="more-view" v-show="showMore">
	<swiper :indicator-dots="true">
		<swiper-item v-for="(swiper,index1) in moreArr" :key="index1">
			<view class="swiper-item" v-for="(list,index2) in swiper" :key="index2">
				<view class="item-wrapper" v-for="item in list" :key="item.id">
					<view class="pic-wrapper" :class="{hidePicWrapper:!item.pic}">
						<image :src="item.pic" @tap="handleMoreFunction(item.flag)"></image>
					</view>
					<view class="text-wrapper">{{item.text}}</view>
				</view>
			</view>
		</swiper-item>
	</swiper>
</view>

......

// 获取指定选择器元素的高度
getHeight(classNa){
	setTimeout(() => {
		const query = uni.createSelectorQuery().in(this);
		query.select(classNa).boundingClientRect(data => {
			this.$emit('heightChange',data.height);
		}).exec();
	},10);
},
// 切换菜单栏显示隐藏
changeMode(){	
	if(this.showMore){
		this.showMore = !this.showMore;
		this.getHeight('.more-view');
	}
},

拿到底部菜单栏的高度后,使用calc计算并修改行内样式,并修改scroll-view的元素内的子元素定位,这里修改scrollToView的值,一定要置空后再修改,否则会修改无效。

<scroll-view class="chat-main" 
 scroll-y="true" 
 :scroll-into-view="scrollToView"
 :scroll-with-animation="needScrollAnimation"
 @scrolltoupper="debounce"
 :style="{height:scrollViewHeight}"
></scroll-view>

...... 

// 弹出菜单栏修改scroll-view高度
handleHeightChange(height){
	this.scrollViewHeight= `calc(100vh - 208rpx - ${height}px - ${this.statusBarHeight}px)`;
	this.scrollToView = '';
	this.$nextTick(function(){		
		this.scrollToView = 'msg' + this.msg[this.msg.length - 1].id;
	})
}

实现一对一聊天

关于websocket

项目中使用的socket.io底层使用到的是websocket协议,可以实现服务器主动推送消息给客户端,一般应用于实时通信,在线支付等场景,虽然socket.io对其进行了封装,但对其原理的了解还是有必要的。

在websock出现之前,一般使用ajax轮询(设置定时器在相同时间间隔内反复发送请求到服务器拿到服务器最新的数据),长轮询(在指定时间内不让当前请求断开),流化技术等手段进行即时通信,这三者都基于http协议实现,但都非常占用服务器的资源, 显著增加了延时。

websocket协议解决这些缺点,它是一种全双工、双向、单套接字的连接,建立在TCP协议之上,当websocket连接建立后,服务器和客户端可以双向通信,具有以下特点:

1)建立在TCP协议之上,服务端的实现比较容易;

2)于HTTP协议有着良好的兼容性,默认的端口也是80和443,并且握手阶段采用HTTP协议;

3)数据格式轻量,性能开销小,通信高效;

4)可以发送文本,也可以发送二进制数据;

5)没有同源限制


http请求响应图解:

客户端发送请求,服务器响应,至此一次请求响应结束,再次获取服务端最新数据,需要再次重复上述过程;

websocket图解:

黄色部分是握手阶段,客户端给服务端发送请求,该请求基于http协议,服务器返回101状态码,代表成功建立连接,随后客户端和服务器可以开始全双工数据交互,且服务器可以主动推送消息给浏览器,下面是websocket的请求报文:

1.使用websocket请求行的路径是以ws开头,代表使用的是websocket协议

2.请求头Connection:Upgrade代表当前服务器这是一个升级的链接

3.请求头Upgrade:websocket代表需要将当前的链接升级为websocket链接

4.请求头Sec-WebSocket-Key: JnoOq+qL9WP3um80g1Sz3A==是客户端使用base64编码的24位随机字符序列,用户服务器标识当链接的客户端,同时要求服务器响应一个同样加密的Sec-WebSocket-Accept头作为应答;


websocket响应报文如下:

1.服务器响应101状态码代表websocket链接建立成功

2.响应头Sec-WebSocket-Accept: Eu6A8ipjouG1LVFt6xFMSrPFk1E=是对客户端请求头Sec-WebSocket-Key的应答,用于给客户端标识当前的服务器


客户端websocket实现

websocket是HTML5的新特性之一,首先你的浏览器必须支持websocket

1.创建WebSocket实例

const ws = new WebSocket('ws:localhost:8000');

参数url:ws://ip地址:端口号/资源名

2.WebSocket对象包含以下事件

open:连接建立时触发

message:客户端接收服务端数据时触发

error:通信发生错误时触发

close:连接关闭时触发

3.WebSocket对象常用方法

send():使用连接给服务端发送数据

客户端websocket代码模板:

;((doc,WebSocket) => {

    const msg = doc.querySelector('#msg');  // 获取输入框,需要发送的消息
    const send = doc.querySelector('#send');  // 发送按钮

    // 创建websocket实例
    const ws = new WebSocket('ws:localhost:8000');
    
    // 初始化
    const init = () => {
        bindEvent();
    }
    
    // 绑定事件
    function bindEvent () {
        send.addEventListener('click',handleSendBtnClick,false);
        ws.addEventListener('open',handleOpen,false);
        ws.addEventListener('close',handleClose,false);
        ws.addEventListener('error',handleError,false);
        ws.addEventListener('message',handleMessage,false);

    }

    function handleSendBtnClick () {
        const message = msg.value;
        
        // 将数据发送给服务器
        ws.send(JSON.stringify({
            message:message
        }));

        msg.value = '';
    }

    function handleOpen () {
        console.log('open');
        // 当连接建立时,一般做一些页面初始化操作
    }
    
    function handleClose () {
        console.log('close');
        // 当连接关闭时
    }

    function handleError () {
        console.log('error');
        // 当连接出现异常时
    }

    function handleMessage (e) {
        // 在这里获取后端广播的数据,数据通过事件对象e活得,数据存放在e.data中
        const showMsg = JSON.parse(e.data);
    }

    init();
})(document,WebSocket)

由此可见,使用原生websocket完全可以进行聊天通信,但是它提供的事件和api有限,对于一些复杂的需求实现起来比较困难,socket.io是一个websocket库,它对于websocket进行了很好的封装,提供了许多api,以及自定义事件,使用起来比较灵活。


聊天功能的前后端交互顺序图

需要实现的是客户端A发送消息给客户端B,客户端B能够自动接收并显示,实现私聊的关键是要确定需要将消息发送给谁,所以在进入聊天界面的的时候,每一个连接服务器的客户端就需要将自己的id告诉服务器,服务器会维护一个对象专门用于存放当前已连接的用户id

客户端A进入聊天界面的的时候,还需要存放客户端B的用户id,在发送消息的时候将客户端B的id传递给服务器,让服务器知道当前的这条消息要发送给谁,服务器收到后就会查询存放用户id的对象,如果客户端B连接那么就将A的消息发送给它,这就是私聊的大致思路。

建立连接

能够实现客户端之间的通信首先需要将客户端与服务器建立连接,首先下载依赖,客户端使用weapp.socket.io,服务端使用socket.io

npm i socket.io@2.3.0 --save
npm i express@4.17.1 --save
npm i weapp.socket.io@2.1.0 --save

为了保证能连接正常,建议下载指定版本,前后端版本不匹配会导致连接失败报错。

官方文档英文:Socket.IO

W3Cschool中文文档:socket.io官方文档_w3cschool

客户端:

客户端下载完毕后,可以将weapp.socket.io.js文件单独拿出,其存放的文件位置如下图

将其放在项目指定文件夹下引入,这里放在socket文件下;随后在项目的main.js中引入使用,这里将io挂载在Vue的原型上,供全局使用,连接地址为服务器的地址,端口号需与服务器socket.io监听的端口保持一致;

import io from './socket/weapp.socket.io.js'
Vue.prototype.socket = io('http://localhost:8000');

服务器:

服务器使用node的express框架搭建,在入口js中配置如下,io.on用于绑定事件,connection事件在连接时触发,它是socket.io内置事件之一。

const express = require('express');
const app = express();
let server = app.listen(8000);
let io = require('socket.io').listen(server);

io.on('connection',(socket) => {   
    console.log("socket.io连接成功");
});

socket.io建立连接会产生跨域问题,这里直接通过express的方式使用CORS解决跨域:

app.all('*', function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With");
  res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
  res.header("X-Powered-By",' 3.2.1')
  if(req.method=="OPTIONS") res.send(200);/*让options请求快速返回*/
  else  next();
});

当然socket.io也提供了跨域的解决方案,具体可见 Handling CORS | Socket.IO

完成以上配置后,启动项目,客户端便可使用socket.io与服务器正常连接。

观察浏览器network选项卡,请求类型为websocket,响应状态码101,可见socket.io的连接底层走的就是websocket协议

存储连接的用户

用户登陆成功跳转到index主页,每一位用户在注册时都会在数据库生成一个唯一的用户id,这里需要将每一个连接成功的用户id发送给服务器

       =>     

socket.io服务端除了connection(socket连接成功之后触发),message(客户端通过socket.send来传送消息时触发此事件),disconneting(socket失去连接时触发,包括关闭浏览器,主动断开,掉线等任何断开连接的情况) 等内置的默认事件外,还可以使用自定义事件,客户端也类似。

API上,使用emit()触发事件,使用on()绑定事件,进入首页后在客户端onLoad中触发自定义事件login,同时从本地存储中取出用户uid,上传服务器

export default {
	data() {
		return {
			uid:'',  // 当前用户id
	},
    onLoad() {
		this.getStroage();
        this.addUserToSocket(this.uid);
    },
    methods:{
        // 获取本地存储
        getStroage(){
			const value = uni.getStorageSync('user');
			if(value){
			    this.uid = value.id;
			} else {
				uni.navigateTo({
					 url:'/pages/login/login'
				})
			}
	    },
        // 添加连接的用户
        addUserToSocket(uid){
		    this.socket.emit('login',uid);
		},
    }
}

在服务端绑定login事件,同时创建对象connectedUsers存放连接的用户, 将用户uid作为key保存,value是socket.id,socket.id是connection回调参数的一个属性,socket.id用于socket.io唯一标识连接的用户

当用户退出应用时触发disconnecting事件,将此用户信息从connectedUsers对象中删除。

let connectedUsers = {}; 
io.on('connection',(socket) => {   
    
    console.log("socket.io连接成功");
    // console.log(socket);
    // 用户进入主页时获取用户id保存
    socket.on('login',(id) => {
        console.log("socket.id:" + socket.id);
        socket.name = id;
        connectedUsers [id] = socket.id;
    });

    // 用户离开
    socket.on('disconnecting',() => {
        console.log('leave:' + socket.id);
        if(users.hasOwnProperty(socket.name)){
            delete connectedUsers [socket.name];
        }
    });
});

总结:

1)io.on可用来给当前socket连接绑定connection事件,参数socket可以获取这次连接的配置信息,最常用的就是socket.id,它是本次连接的唯一标识

io.on('connection',function(socket){ ...... })

2)on用于绑定事件,用于接收传递的数据

socket.on('自定义事件名',function(参数1,参数2,......,参数n) { ...... });

3)emit用于触发事件,用于传递数据

socket.emit('自定义事件名',参数1,参数2,......,参数n);

4)disconnecting在失去连接时时触发,断开可能是关闭浏览器,主动断开,掉线等导致

socket.on('disconnecting',() => {})

发送聊天信息 

客户端发送消息,将聊天内容加工处理后,触发自定义事件msg,将内容,发送者id和接收者id发送给服务器,代码如下:

客户端chatroom.vue:

// 发送聊天数据
sendSocket(msg){
	if(this.type === '0'){
		// 1对1聊天
		this.socket.emit('msg',msg,this.uid,this.fid);
	} else {
		// 群消息
		this.socket.emit('gmsg',msg,this.uid,this.fid);
	}
},

服务器绑定msg事件,得到客户端发来数据,首先需要操作数据库完成插入最新的聊天内容,更改最后的通讯时间等操作,如果对方用户在线,则connectedUsers 对象中必然存在该用户的id,使用socket.to(指定接收者的socket.io)将消息发送给指定的用户,同时触发自定义事件backMsg,用法如下:

发送给指定 socketid 的客户端(私密消息)

socket.to(<socketid>).emit('自定义事件名', 参数);

注意:如果不使用socket.to方法直接调用emit,则会发送给所有在线的用户。 

服务器代码:

// 引入数据库文件
let dataBase= require("./dataBase");
// 1对1消息发送
socket.on('msg',(msg,fromId,toId) => {
    console.log('服务器收到用户' + fromId + '发送给' + toId + '的消息')
    console.log('发送的消息是:',msg);
    // 修改好友最后通讯时间
    dataBase.updateLastMsgTime(fromId,toId);
    dataBase.updateLastMsgTime(toId,fromId);
    // 添加消息
    dataBase.insertMsg(fromId,toId,msg.message,msg.types);
    console.log('数据库插入成功');
    
    // 将获取的消息发送给好友,users[toId]就是好友的socket.id
    if(connectedUsers[toId]){
        console.log('将消息发送给',toId,'成功');
        socket.to(connectedUsers[toId]).emit('backMsg',msg,fromId,0);
    }   
});

这样客户端绑定backMsg事件,就能拿到发送消息了!处理消息展示即可,但需要判断当前用户此时打开的聊天界面是否就是当前发送者聊天对话框即if(fromId === this.fid && type === 0),否则会造成聊天内容的错误展示,比如当前用户可能存在多个好友,客户端A给客户端B发消息时B打开的是和C的聊天对话框,此时就会在C的对话框中错误的收到A发来的消息

客户端chatroom.vue:

this.socket.on('backMsg',(msg,fromId,type) => {
	// 如果是1对1消息fromId是当前聊天窗口的好友id时执行
	if(fromId === this.fid && type === 0){
		......
		// 一条聊天记录
		let newMsg = {
			fromId:fromId,
			id:msg.id,
			imgUrl:msg.imgUrl,
			message:msg.message,
			types:msg.types,  // 0 - 文字信息,1 - 图片信息, 2 - 音频
			time:new Date(),
			isFirstPlay:true,
		};
		this.msg.push(newMsg);  
		// 如果消息是图片
		if(msg.types === '1') {
			this.msgImage.push(msg.message)
		}
		this.$nextTick(function(){
			this.scrollToView = 'msg' + this.msg[this.msg.length - 1].id;
		});
		......
	}	
});

测试效果如下: 

 

服务器终端输出结果如下:

首页新消息提示

如下图,用户有新消息会在首页及时显示,并提示未读消息数量

 

需要给首页绑定获取消息的自定义事件backMsg,绑定时机是在生命周期onLoad中,事件一旦触发代表有好友向你发送消息了,会获取服务器传来的消息,在事件回调中要完成两个操作,首先查找发来新消息的好友在首页好友列表数组的索引下标,随后修改指定的数组元素内容,更新这个好友最后消息的时间、最后消息的内容、未读消息数;并将该元素现有位置删除,添加到整个数组的头部,即把这个好友item放到首页列表的最上方,首页index.vue相关代码如下:

<view class="fl-wrapper">
	<view class="friend-list" v-for="(item,index) in friends" :key="index" @tap="toChatInterface(item)">
        <!-- 用户头像 -->
		<view class="avator">
            <!-- 未读消息数 -->
			<view class="new-message-number" v-show="item.unreadMsg">{{item.unreadMsg}}</view>
			<image :src="item.imgUrl" class="img" ></image>
		</view>
		<view class="wrapper-right">
			<view class="wrapper-right-left">
                <!-- 好友名 最后聊天时间 -->
				<view class="text">
					<view class="name">{{item.nickName}}</view>
					<view class="time">{{getTime(item.lastTime)}}</view>
				</view>
                <!-- 最后聊天消息 -->
				<view class="message" v-if="item.lastMsgType==='0'">{{item.lastMsgUsername ? item.lastMsgUsername : ''}}{{item.lastMsg}}</view>
				<view class="message" v-if="item.lastMsgType==='1'">{{item.lastMsgUsername ? item.lastMsgUsername : ''}}[图片]</view>
				<view class="message" v-if="item.lastMsgType==='2'">{{item.lastMsgUsername ? item.lastMsgUsername : ''}}[语音]</view>
				<view class="message" v-if="item.lastMsgType==='3'">{{item.lastMsgUsername ? item.lastMsgUsername : ''}}[位置]</view>
			</view>						
		</view>					
	</view>
</view>

......

onLoad() {
    this.receiveSocket('backMsg');
}
methods:{
    // 接收个人/群聊天信息
    receiveSocket(eventName){
	    this.socket.on(eventName,(msg,fromId,type) => {
		    if(type === 0){
			    let index;
			    if(eventName == 'backMsg') {
                    // 获取有新消息的好友在整个好友数组中的索引下标
				    index = this.friends.findIndex((item) => {
				    	return item.id === fromId
				    });
		    	}
		    	// 修改未读消息数 
		    	this.getUnreadMsg(this.friends[index]);
                // 修改最后聊天时间
		    	this.friends[index].lastTime = msg.time;
                // 修改最后聊天信息
		    	this.friends[index].lastMsg = msg.message;
                // 修改最后聊天信息的类型
		    	this.friends[index].lastMsgType = msg.types;
                
                // 删除当前item,将其插入到数组的首部,即展示在列表最上方
	    		const tempItem = this.friends[index];
	    		this.friends.splice(index,1);
	    		this.friends.unshift(tempItem);
	    	}
    	});
    },
}

此外还有一个问题就是何时清空未读消息数,清空的操作需要进行两次,一次是用户进入聊天页面时进行清空,在聊天页生命周期onLoad中调用清空消息数的后端接口,清空现有的未读消息;另一次是在点击返回按钮如下图,返回首页时清空,在此按钮事件的回调中调用清空未读消息数的接口,这是为了清空用户和他人聊天时已读的消息,两次操作缺一不可。

实现群聊

群聊的前后端顺序图如下所示:

需要实现的是客户端A在群内发送了消息后,在同一群内的客户端BCD都能同时收到A发送的消息。群聊的大致思路和私聊基本相似,不同点在于群聊中引入了房间的概念,在房间内的成员就是这个群聊的群成员,任何群成员的群内发言就会在这个房间内进行广播所有在线的群成员都能及时够收到。

加入房间

使用socket.join()加入房间,具体使用如下:

socket.join('room',function(){ ...... });

room:房间id,是一个字符串,用户自定义,加入房间会触发参数二回调

socket.leave(room,function(){ ...... })

与join相对应的是leave方法,即退出指定的房间,参数二异常回调函数为可选值。需要注意的是,当与客户端断开连接时,会自动将其从加入的房间中移除

在这个项目里房间id使用的是每一个群聊的群id号,它可以唯一标识一个群聊;

加入房间的操作同样是在用户登录成功进入首页时进行,一个用户可能加入了多个群聊,那么在主页请求用户群聊接口后,需要依次遍历接口返回的群聊列表,为每一个群聊触发addGroup事件,将当前的群id发送给后端,让当前用户加入每个群聊的房间。

index.vue

// 获取当前用户的群消息
getGroup(){
	uni.request({
		url:`${this.baseUrl}/index/getGroupList`,
		method:'POST',
		data:{
			uid:this.uid,  // 用户id
		},
		success: (res) => {
			let data = res.data.result;
            // 遍历当前用户的群列表
			for (var i = 0; i < data.length; i++) {
				......
                // 触发addGroup事件,携带群id,加入房间
				this.socket.emit('addGroup',data[i].id);
			}            
            ......
		}
	});
},

 服务器绑定addGroup事件,调用socket.join,让当前用户连接加入房间号为groupId的房间

io.on('connection',(socket) => { 
    // 加入群
    socket.on('addGroup',(groupId) => {
        console.log('用户',socket.id,'加入了groupId为',groupId,'的群聊');
        socket.join(groupId);
    });
}

效果:例如当前这个用户加入了三个群聊,首页加载后就会触发addGroup三次,依次加入这三个群id标识的房间。

服务器终端输出效果如下:

发送群消息

某一群成员在群内发送消息,会和私聊同样的方式将语音和图片这些静态资源上传服务器,返回服务器存放地址后进行封装,触发gmsg事件将处理后的消息提交服务器

// 发送聊天数据
sendSocket(msg){
	if(this.type === '0'){
		// 1对1聊天
		this.socket.emit('msg',msg,this.uid,this.fid);
	} else {
		// 群消息
		this.socket.emit('gmsg',msg,this.uid,this.fid);
	}
},

群内广播消息使用到的api是socket.to,具体使用如下:

将内容发送给同在房间名roomName的所有客户端,除了发送者

socket.to(roomName).emit('事件名',参数1,参数2,......参数n);

如果需要包含发送者可以使用

io.in(roomName).emit('事件名',参数1,参数2,......参数n);

也可以同时发送给在多间房间的客户端,使用to链式调用的形式,不包含发送者

socket.to(roomName1).to(roomName2).emit('事件名',参数1,参数2,......参数n);

当然,当前项目中只需要使用第一种方式即可

服务器的gmsg事件回调中,同样需要将获取到的消息插入数据库,同时修改群最后通信时间以及全体成员的未读消息数,最后调用 socket.to方法,触发groupMsg事件,将消息发送给群聊内的其它在线用户。

// 引入数据库文件
let dataBase = require("./dataBase");
// 接收群消息
socket.on('gmsg',(msg,fromId,groupId) => {
    console.log('服务器接收到来自群',groupId,'的用户',fromId,'的消息',msg);
    // 修改群的最后通信时间
    dataBase.updateGroupLastTime(groupId);
    // 添加群消息
    dataBase.insertGroupMsg(fromId,groupId,msg.message,msg.types);
    //将所有成员的未读消息数加一
    dataBase.changeGroupUnreadMsgNum(groupId);
    console.log('消息',msg.message,'插入数据库成功')
    // 获取当前用户的名字和头像
    dataBase.userDetails(fromId).then((data) => {
        console.log('查询发送者用户名成功,用户名是:',data[0]);
        console.log('正在将信息',msg.message,'发送至群',groupId,'内');
        // 群内广播消息
        socket.to(groupId).emit('groupMsg',msg,fromId,0,data[0].name,groupId);
    });
});

客户端在线群成员收到消息,执行groupMsg事件回调中的方法,内部大致逻辑和私聊完全一致,可以将其封装成公共方法使用,需要注意的依旧是要做群id一致性判断,防止获取的消息显示在其它聊天窗口中,即 if(fromId !== this.uid && groupId === this.fid)。

this.socket.on('groupMsg',(msg,fromId,type,friendName,groupId) => {
	// 判断当前打开的群id和接收消息的群id是否一致,防止消息错误显示
	if(fromId !== this.uid && groupId === this.fid){
		......
		// 模拟服务器数据
		let newMsg = {
			fromId:fromId,
			id:msg.id,
			imgUrl:msg.imgUrl,
			message:msg.message,
			types:msg.types,  // 0 - 文字信息,1 - 图片信息, 2 - 音频
			time:new Date(),
			isFirstPlay:true,
			friendName:friendName   // 群需显示发送消息用户的名字
		};
		this.msg.push(newMsg);
		// 如果消息是图片
		if(msg.types === '1') {
			this.msgImage.push(msg.message)
		}
		this.$nextTick(function(){
			this.scrollToView = 'msg' + this.msg[this.msg.length - 1].id;
		});
		......
	}	
});

效果演示:输入一段文字发送到群内

服务器此时终端输出如下

以上就是项目聊天功能难点的全部内容,前端实现实时聊天主要就是对于socket.io提供api的合理使用,剩余的难点就是页面显示的部分逻辑处理,用户体验的优化,还可以在此基础上添加更多的功能,若有不足之处恳请指正!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/405255.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue3生命周期及setup介绍

&#x1f337; 生命周期 下图对比了vue3&#xff08;左&#xff09;和vue2&#xff08;右&#xff09;的生命周期&#xff1a;vue3将destoryed该名成了unmounted&#xff0c;相应的beforeDestory改成了beforeUnmounted。除此之外在组合式API中新增了个钩子函数&#xff1a;set…

vue项目:大屏自适应解决方案(两种)

css缩放方案&#xff1a; 利用transform&#xff1a;scale 进行适配 推荐使用v-scale-screen 值得注意的是&#xff1a; vue 2.6、2.7 要使用 npm install v-scale-screen1.0.2 vue3&#xff1a;要使用v-scale-screen版本 npm install v-scale-screen2.0.0 用法&#x…

解决npm ERR! Cannot read properties of null (reading ‘pickAlgorithm‘)

文章目录1. 复现问题2. 分析问题3. 解决问题1. 复现问题 今天准备克隆Redis桌面(GUI)管理客户端&#xff0c;故按照官方文档给出的指令运行时&#xff0c;如下图所示&#xff1a; 但在执行指令npm install --platformwin32却报出如下图错误&#xff1a; PS D:\Software\Redis…

Vue中使用Datav 完成大屏基本布局

效果图 大屏前言 在实际开发过程中&#xff0c;我们经常需要一个大屏进行一些常规数据的展示。在Vue中也是提供了这样的容器组件 我们可以使用基于Vue的 Datav组件 Vue-Baidu-Map地图组件 Echarts图表组件 时间戳就实现基本布局 在Vue中配置大屏路由的时候&#xff0c;我们…

vue播放rtsp视频流

工作有要播放视频监控的需求&#xff0c;最近就自己先了解了一下网页播放rtsp视频流的方法&#xff0c;以下是我的个人经验。 最终选择了vuewebrtc-streamer实现在网页播放rtsp流这种方法进行测试。 个人经验 第一次尝试了vue-video-playervideojs的方法&#xff0c;发现只适…

WPS JS宏入门案例集锦

JS宏官方API文档&#xff1a;https://qn.cache.wpscdn.cn/encs/doc/office_v19/index.htm 批量创建工作表/簿 批量创建工作表&#xff1a; function 批量创建工作表(){for (var city of ["成都","上海","北京"]){let sht Worksheets.Add();s…

无需本地部署 在线使用Stable Diffusion Webui 使用共享模型

尝试本地部署Stable Diffusion的时候遇到了很多的麻烦&#xff0c;自己训练AI也非常的麻烦&#xff0c;可以尝试使用Webui使用别人上传的模型第一步进入网站https://github.com/camenduru/stable-diffusion-webui-colab向下拉到readme第一个 stable_diffusion_webui_colab&…

前端常见八大设计模式

一、设计模式是什么&#xff1f; 设计模式是在某种场合下对某个问题的一种解决方案。设计模式是通过概念总结出来的模版&#xff0c;总结出来的固定的东西。每一个模式描述了一个在我们周围不断重复发生的问题&#xff0c;以及该问题的解决方案的核心。 二、设计原则–设计模…

给女友的网页小惊喜,(生日,周年,表白通用) ☞谁说程序员不懂浪漫

有女朋友的拿去给女朋友一个惊喜&#xff0c;没女朋友的拿去表白&#xff0c;或者NEW它10000000个&#xfeff;ε≡٩(๑>₃<)۶ 文章目录前言适用范围网页展示登录界面文字界面图片界面尾部界面获取源码前言 前些日子是女友的一周年&#xff0c;康康想用一种特殊的方式…

vue实现导出word文档(含多张图片)

一、实现效果 以填写并导出房屋出租审批表为例&#xff0c;首先填写表格相应内容后&#xff0c;点击" 导出 "按钮实现word文档的导出功能&#xff0c;界面如下所示&#xff1a; 最后导出word文档如下所示&#xff1a; 二、所需插件 这里使用npm对以下所需依赖进…

【SpringBoot+Vue】全网最简单但实用的前后端分离项目实战笔记 - 前端

配套视频地址&#xff1a;https://www.bilibili.com/video/BV1dG4y1T7yp/ 前端笔记 1. node环境 官网&#xff1a;https://nodejs.org 注意&#xff0c;node可以比我稍低&#xff0c;但不要更高 2. 下载vue-admin-template https://panjiachen.gitee.io/vue-element-admin…

HTML表格合并行和列

HTML表格合并行和列1.合并行&#xff1a;rowspan2.合并列&#xff1a;colspan1.合并行&#xff1a;rowspan 在设计表格时&#xff0c;有时我们需要将“横向的N个单元格”或者“纵向的N个单元格”合并成一个单元格&#xff08;类似Word的表格合并&#xff09;&#xff0c;这个时…

2023前端最新高频面试题总结(附答案)

目录 1.vue双向数据绑定的原理&#xff1f; 2.vue的生命周期有哪些 3.v-if 和v-show有什么区别&#xff1f; 4.async await 是什么&#xff1f;它有哪些作用&#xff1f; 5、数组常用的方法&#xff1f;哪些方法会改变原数组&#xff0c;哪些不会 6.什么是原型链&#xf…

前端网页设计必逛的六个宝藏网站(非常值得收藏)

&#x1f389;个人主页&#xff1a;这个昵称我想了20分钟 ✨往期专栏&#xff1a; 【速成之路】jQuery 【SQL server速成之路】 素材网站✨iconfont阿里巴巴矢量图标库  ✨美叶  ✨IconPark  ✨pexels  ✨COLOR  ✨Uigradients✨iconfont阿里巴巴矢量图标库 网站入口…

【微信小程序】使用uni-app——开发首页搜索框导航栏(可同时兼容APP、H5、小程序)

目录 前言 App、H5效果 小程序效果 一、兼容APP、H5的方式 二、兼容小程序 三、实现同时兼容 前言 首页都会提供一个搜索框给到客户&#xff0c;让客户自己去搜索自己想要的内容&#xff0c;这里就需要导航栏&#xff0c;来实现搜索页面的跳转&#xff0c;效果如下 App…

Vue框架教程-从入门到项目实战

创建Vue项目 我们通过vue-cli创建一个vue项目&#xff0c; 在cmd窗口输入 vue ui 进入vue-cli可视化界面&#xff08;如果无效请升级vue-cli版本&#xff09;点击创建&#xff0c;选择一个项目目录输入项目名称和git初始化窗口(可选)选择预设&#xff0c;可以选择手动和预定的…

如何清除全部的定时器

通过 setTimeout() 函数来建立定时器&#xff0c;并通过 clearTimeout() 函数来清除定时器。 let timerTimeout setTimeout(() > {console.log("2222222-----------------------------"); }, 1000); clearTimeout(timerTimeout);通过 setInterval() 函数来建立定…

Node.js error: ERR_OSSL_EVP_UNSUPPORTED

Node.js 17版本的更新日志&#xff1a; 2021-10-19, Version 17.0.0 (Current), BethGriggs Notable Changes ... OpenSSL 3.0 Node.js now includes OpenSSL 3.0, specifically quictls/openssl which provides QUIC support. With OpenSSL 3.0 FIPS support is again availab…

npm : 无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。

报错起因 在VScode中运行vue项目时&#xff0c;【前提&#xff1a;把项目文件换到另一个网盘存放&#xff0c;存放失败&#xff0c;又重新放回原位置再次运行时】 报错如下&#xff1a; npm : 无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查 解决…

C++——WebServer服务器项目

项目场景&#xff1a; C——WebServer服务器编程 项目搭建 &#xff08;1&#xff09;配置虚拟机&#xff0c;下载XShell、Xftp以及windows版本的VScode&#xff1b; &#xff08;2&#xff09;安装SSH&#xff1a; sudo apt install openssh-server&#xff08;3&#xff…