欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

uni-app小程序?qū)崿F(xiàn)微信在線聊天功能(私聊/群聊)

 更新時(shí)間:2023年02月18日 11:22:29   作者:玖伴_  
這篇文章主要介紹了uni-app小程序?qū)崿F(xiàn)微信在線聊天(私聊/群聊),今天記錄一下項(xiàng)目核心功能的實(shí)現(xiàn)過程。頁面UI以及功能邏輯全部來源于微信,即時(shí)聊天業(yè)務(wù)的實(shí)現(xiàn)使用socket.io,前端使用uni-app開發(fā),后端服務(wù)器基于node實(shí)現(xiàn),數(shù)據(jù)庫選擇mongoDB,需要的朋友可以參考下

之前學(xué)習(xí)使用uni-app簡單實(shí)現(xiàn)一個(gè)在線聊天的功能,今天記錄一下項(xiàng)目核心功能的實(shí)現(xiàn)過程。頁面UI以及功能邏輯全部來源于微信,即時(shí)聊天業(yè)務(wù)的實(shí)現(xiàn)使用socket.io,前端使用uni-app開發(fā),后端服務(wù)器基于node實(shí)現(xiàn),數(shù)據(jù)庫選擇mongoDB。

首先在系統(tǒng)中注冊兩個(gè)用戶,將對方添加為好友后,開始正常聊天,先簡單看一下聊天功能的效果圖,分為私聊和群聊兩大部分

一對一聊天效果:

在好友列表中添加群成員創(chuàng)建群后即可群聊,群聊效果:

聊天信息列表的渲染

聊天信息列表區(qū)域是一個(gè)滾動(dòng)區(qū),這里使用scroll-view組件,其中對于聊天信息展示,主要分為自己的消息和好友的消息,自己的消息位于右側(cè),好友的消息位于左側(cè),所以靜態(tài)頁面階段要實(shí)現(xiàn)是左側(cè)消息和右側(cè)消息的頁面布局,以及這些消息類型為文字,圖片,語音,位置信息時(shí)的布局。

后端接口返回的聊天信息是按照時(shí)間順序排列的,渲染聊天信息時(shí)使用v-for遍歷接口返回的消息列表的內(nèi)容即可,需要注意的是,還需要使用條件渲染v-if根據(jù)每一條消息的發(fā)送者id和當(dāng)前用戶的id判斷消息的發(fā)送方和接受方,渲染在左右指定的區(qū)域,當(dāng)前用戶的id從本地存儲localStorage中獲?。贿€有就是使用條件渲染判斷消息的類型,是文字,圖片,語音或定位,合理展示。

<!-- 一條聊天記錄  -->
<view class="chat-item" v-for="(item,index) in msg" :key="item.id">
    <!-- 時(shí)間  -->
	<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>

聊天信息發(fā)送的相關(guān)問題

點(diǎn)擊發(fā)送按鈕,正式將信息發(fā)送給服務(wù)器之前,還有幾個(gè)問題需要解決,這里面有許多坑,在實(shí)現(xiàn)的時(shí)候走了不少彎路。

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

如下圖,當(dāng)發(fā)送了一條聊天信息時(shí),聊天信息列表就會增加這條消息,之所以能夠看到這條消息,那是因?yàn)閟croll-view的滾動(dòng)條在消息添加時(shí)將位置定位到了最底部,這是需要進(jìn)行一些處理的,默認(rèn)效果是這樣的

是不是很變扭?這樣的用戶體驗(yàn)很差,滾動(dòng)條不會自動(dòng)定位到底部,這里需要給scroll-view組件添加一個(gè)scroll-into-view屬性,按照官方文檔的說法它的值應(yīng)為某子元素id。設(shè)置哪個(gè)方向可滾動(dòng),則在哪個(gè)方向滾動(dòng)到該元素,也就是說可以動(dòng)態(tài)的修改這個(gè)屬性的值,從而讓scroll-view組件的滾動(dòng)到想要滾動(dòng)的頁面元素位置。

這里就給每一個(gè)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>

在發(fā)送消息的方法中修改scroll-into-view的值scrollToView,讓其為最新一條聊天記錄即msg.length - 1的id值,必須使用在$nextTick回調(diào)中,這是為了在新的聊天記錄渲染完畢后再去定位。

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

 這樣才能實(shí)現(xiàn)最終的效果

 

2.如何動(dòng)態(tài)修改scroll-view的高度

如下圖,點(diǎn)擊 + 按鈕發(fā)送位置信息時(shí)會彈出底部菜單欄,但此時(shí)scroll-view內(nèi)的聊天內(nèi)容會被覆蓋,用戶想要看最后一條記錄還需操作滾動(dòng)條,這也是不好的用戶體驗(yàn)。

需要做到的是彈出底部菜單欄的同時(shí)減小聊天內(nèi)容區(qū)域scroll-view組件的高度,讓用戶能夠完整的看到最后的聊天記錄。

需要獲取底部菜單欄彈出的高度,隨后讓scroll-view組件減少這部分高度即可。在uni-app中無法操作dom,獲取元素的尺寸使用createSelectorQuery獲取頁面節(jié)點(diǎn),再用 boundingClientRect查詢節(jié)點(diǎn)的尺寸。官方文檔:uni.createSelectorQuery() | uni-app官網(wǎng)

使用如下代碼獲取頁面節(jié)點(diǎn)的尺寸,可能無法及時(shí)獲取到(得到的可能是undefined),這里需要用定時(shí)器包裹,才能拿到菜單欄的高度

<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計(jì)算并修改行內(nèi)樣式,并修改scroll-view的元素內(nèi)的子元素定位,這里修改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;
	})
}

實(shí)現(xiàn)一對一聊天

關(guān)于websocket

項(xiàng)目中使用的socket.io底層使用到的是websocket協(xié)議,可以實(shí)現(xiàn)服務(wù)器主動(dòng)推送消息給客戶端,一般應(yīng)用于實(shí)時(shí)通信,在線支付等場景,雖然socket.io對其進(jìn)行了封裝,但對其原理的了解還是有必要的。

在websock出現(xiàn)之前,一般使用ajax輪詢(設(shè)置定時(shí)器在相同時(shí)間間隔內(nèi)反復(fù)發(fā)送請求到服務(wù)器拿到服務(wù)器最新的數(shù)據(jù)),長輪詢(在指定時(shí)間內(nèi)不讓當(dāng)前請求斷開),流化技術(shù)等手段進(jìn)行即時(shí)通信,這三者都基于http協(xié)議實(shí)現(xiàn),但都非常占用服務(wù)器的資源, 顯著增加了延時(shí)。

websocket協(xié)議解決這些缺點(diǎn),它是一種全雙工、雙向、單套接字的連接,建立在TCP協(xié)議之上,當(dāng)websocket連接建立后,服務(wù)器和客戶端可以雙向通信,具有以下特點(diǎn):

1)建立在TCP協(xié)議之上,服務(wù)端的實(shí)現(xiàn)比較容易;

2)于HTTP協(xié)議有著良好的兼容性,默認(rèn)的端口也是80和443,并且握手階段采用HTTP協(xié)議;

3)數(shù)據(jù)格式輕量,性能開銷小,通信高效;

4)可以發(fā)送文本,也可以發(fā)送二進(jìn)制數(shù)據(jù);

5)沒有同源限制

http請求響應(yīng)圖解:

客戶端發(fā)送請求,服務(wù)器響應(yīng),至此一次請求響應(yīng)結(jié)束,再次獲取服務(wù)端最新數(shù)據(jù),需要再次重復(fù)上述過程;

websocket圖解:

黃色部分是握手階段,客戶端給服務(wù)端發(fā)送請求,該請求基于http協(xié)議,服務(wù)器返回101狀態(tài)碼,代表成功建立連接,隨后客戶端和服務(wù)器可以開始全雙工數(shù)據(jù)交互,且服務(wù)器可以主動(dòng)推送消息給瀏覽器,下面是websocket的請求報(bào)文:

1.使用websocket請求行的路徑是以ws開頭,代表使用的是websocket協(xié)議

2.請求頭Connection:Upgrade代表當(dāng)前服務(wù)器這是一個(gè)升級的鏈接

3.請求頭Upgrade:websocket代表需要將當(dāng)前的鏈接升級為websocket鏈接

4.請求頭Sec-WebSocket-Key: JnoOq+qL9WP3um80g1Sz3A==是客戶端使用base64編碼的24位隨機(jī)字符序列,用戶服務(wù)器標(biāo)識當(dāng)鏈接的客戶端,同時(shí)要求服務(wù)器響應(yīng)一個(gè)同樣加密的Sec-WebSocket-Accept頭作為應(yīng)答;

websocket響應(yīng)報(bào)文如下:

1.服務(wù)器響應(yīng)101狀態(tài)碼代表websocket鏈接建立成功

2.響應(yīng)頭Sec-WebSocket-Accept: Eu6A8ipjouG1LVFt6xFMSrPFk1E=是對客戶端請求頭Sec-WebSocket-Key的應(yīng)答,用于給客戶端標(biāo)識當(dāng)前的服務(wù)器

客戶端websocket實(shí)現(xiàn)

websocket是HTML5的新特性之一,首先你的瀏覽器必須支持websocket

1.創(chuàng)建WebSocket實(shí)例

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

參數(shù)url:ws://ip地址:端口號/資源名

2.WebSocket對象包含以下事件

open:連接建立時(shí)觸發(fā)

message:客戶端接收服務(wù)端數(shù)據(jù)時(shí)觸發(fā)

error:通信發(fā)生錯(cuò)誤時(shí)觸發(fā)

close:連接關(guān)閉時(shí)觸發(fā)

3.WebSocket對象常用方法

send():使用連接給服務(wù)端發(fā)送數(shù)據(jù)

客戶端websocket代碼模板:

;((doc,WebSocket) => {
 
    const msg = doc.querySelector('#msg');  // 獲取輸入框,需要發(fā)送的消息
    const send = doc.querySelector('#send');  // 發(fā)送按鈕
 
    // 創(chuàng)建websocket實(shí)例
    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;
        
        // 將數(shù)據(jù)發(fā)送給服務(wù)器
        ws.send(JSON.stringify({
            message:message
        }));
 
        msg.value = '';
    }
 
    function handleOpen () {
        console.log('open');
        // 當(dāng)連接建立時(shí),一般做一些頁面初始化操作
    }
    
    function handleClose () {
        console.log('close');
        // 當(dāng)連接關(guān)閉時(shí)
    }
 
    function handleError () {
        console.log('error');
        // 當(dāng)連接出現(xiàn)異常時(shí)
    }
 
    function handleMessage (e) {
        // 在這里獲取后端廣播的數(shù)據(jù),數(shù)據(jù)通過事件對象e活得,數(shù)據(jù)存放在e.data中
        const showMsg = JSON.parse(e.data);
    }
 
    init();
})(document,WebSocket)

由此可見,使用原生websocket完全可以進(jìn)行聊天通信,但是它提供的事件和api有限,對于一些復(fù)雜的需求實(shí)現(xiàn)起來比較困難,socket.io是一個(gè)websocket庫,它對于websocket進(jìn)行了很好的封裝,提供了許多api,以及自定義事件,使用起來比較靈活。

聊天功能的前后端交互順序圖

需要實(shí)現(xiàn)的是客戶端A發(fā)送消息給客戶端B,客戶端B能夠自動(dòng)接收并顯示,實(shí)現(xiàn)私聊的關(guān)鍵是要確定需要將消息發(fā)送給誰,所以在進(jìn)入聊天界面的的時(shí)候,每一個(gè)連接服務(wù)器的客戶端就需要將自己的id告訴服務(wù)器,服務(wù)器會維護(hù)一個(gè)對象專門用于存放當(dāng)前已連接的用戶id。

客戶端A進(jìn)入聊天界面的的時(shí)候,還需要存放客戶端B的用戶id,在發(fā)送消息的時(shí)候?qū)⒖蛻舳薆的id傳遞給服務(wù)器,讓服務(wù)器知道當(dāng)前的這條消息要發(fā)送給誰,服務(wù)器收到后就會查詢存放用戶id的對象,如果客戶端B連接那么就將A的消息發(fā)送給它,這就是私聊的大致思路。

建立連接

能夠?qū)崿F(xiàn)客戶端之間的通信首先需要將客戶端與服務(wù)器建立連接,首先下載依賴,客戶端使用weapp.socket.io,服務(wù)端使用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

為了保證能連接正常,建議下載指定版本,前后端版本不匹配會導(dǎo)致連接失敗報(bào)錯(cuò)。

官方文檔英文:Socket.IO

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

客戶端:

客戶端下載完畢后,可以將weapp.socket.io.js文件單獨(dú)拿出,其存放的文件位置如下圖

將其放在項(xiàng)目指定文件夾下引入,這里放在socket文件下;隨后在項(xiàng)目的main.js中引入使用,這里將io掛載在Vue的原型上,供全局使用,連接地址為服務(wù)器的地址,端口號需與服務(wù)器socket.io監(jiān)聽的端口保持一致;

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

服務(wù)器:

服務(wù)器使用node的express框架搭建,在入口js中配置如下,io.on用于綁定事件,connection事件在連接時(shí)觸發(fā),它是socket.io內(nèi)置事件之一。

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建立連接會產(chǎn)生跨域問題,這里直接通過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();
});

當(dāng)然socket.io也提供了跨域的解決方案,具體可見 Handling CORS | Socket.IO

完成以上配置后,啟動(dòng)項(xiàng)目,客戶端便可使用socket.io與服務(wù)器正常連接。

觀察瀏覽器network選項(xiàng)卡,請求類型為websocket,響應(yīng)狀態(tài)碼101,可見socket.io的連接底層走的就是websocket協(xié)議

存儲連接的用戶

用戶登陸成功跳轉(zhuǎn)到index主頁,每一位用戶在注冊時(shí)都會在數(shù)據(jù)庫生成一個(gè)唯一的用戶id,這里需要將每一個(gè)連接成功的用戶id發(fā)送給服務(wù)器

       =>     

socket.io服務(wù)端除了connection(socket連接成功之后觸發(fā)),message(客戶端通過socket.send來傳送消息時(shí)觸發(fā)此事件),disconneting(socket失去連接時(shí)觸發(fā),包括關(guān)閉瀏覽器,主動(dòng)斷開,掉線等任何斷開連接的情況) 等內(nèi)置的默認(rèn)事件外,還可以使用自定義事件,客戶端也類似。

API上,使用emit()觸發(fā)事件,使用on()綁定事件,進(jìn)入首頁后在客戶端onLoad中觸發(fā)自定義事件login,同時(shí)從本地存儲中取出用戶uid,上傳服務(wù)器

export default {
	data() {
		return {
			uid:'',  // 當(dāng)前用戶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);
		},
    }
}

在服務(wù)端綁定login事件,同時(shí)創(chuàng)建對象connectedUsers存放連接的用戶, 將用戶uid作為key保存,value是socket.id,socket.id是connection回調(diào)參數(shù)的一個(gè)屬性,socket.id用于socket.io唯一標(biāo)識連接的用戶。

當(dāng)用戶退出應(yīng)用時(shí)觸發(fā)disconnecting事件,將此用戶信息從connectedUsers對象中刪除。

let connectedUsers = {}; 
io.on('connection',(socket) => {   
    
    console.log("socket.io連接成功");
    // console.log(socket);
    // 用戶進(jìn)入主頁時(shí)獲取用戶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];
        }
    });
});

總結(jié):

1)io.on可用來給當(dāng)前socket連接綁定connection事件,參數(shù)socket可以獲取這次連接的配置信息,最常用的就是socket.id,它是本次連接的唯一標(biāo)識

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

2)on用于綁定事件,用于接收傳遞的數(shù)據(jù)

socket.on('自定義事件名',function(參數(shù)1,參數(shù)2,......,參數(shù)n) { ...... });

3)emit用于觸發(fā)事件,用于傳遞數(shù)據(jù)

socket.emit('自定義事件名',參數(shù)1,參數(shù)2,......,參數(shù)n);

4)disconnecting在失去連接時(shí)時(shí)觸發(fā),斷開可能是關(guān)閉瀏覽器,主動(dòng)斷開,掉線等導(dǎo)致

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

發(fā)送聊天信息 

客戶端發(fā)送消息,將聊天內(nèi)容加工處理后,觸發(fā)自定義事件msg,將內(nèi)容,發(fā)送者id和接收者id發(fā)送給服務(wù)器,代碼如下:

客戶端chatroom.vue:

// 發(fā)送聊天數(shù)據(jù)
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);
	}
},

服務(wù)器綁定msg事件,得到客戶端發(fā)來數(shù)據(jù),首先需要操作數(shù)據(jù)庫完成插入最新的聊天內(nèi)容,更改最后的通訊時(shí)間等操作,如果對方用戶在線,則connectedUsers 對象中必然存在該用戶的id,使用socket.to(指定接收者的socket.io)將消息發(fā)送給指定的用戶,同時(shí)觸發(fā)自定義事件backMsg,用法如下:

發(fā)送給指定 socketid 的客戶端(私密消息)

socket.to(<socketid>).emit('自定義事件名', 參數(shù));

注意:如果不使用socket.to方法直接調(diào)用emit,則會發(fā)送給所有在線的用戶。 

服務(wù)器代碼:

// 引入數(shù)據(jù)庫文件
let dataBase= require("./dataBase");
// 1對1消息發(fā)送
socket.on('msg',(msg,fromId,toId) => {
    console.log('服務(wù)器收到用戶' + fromId + '發(fā)送給' + toId + '的消息')
    console.log('發(fā)送的消息是:',msg);
    // 修改好友最后通訊時(shí)間
    dataBase.updateLastMsgTime(fromId,toId);
    dataBase.updateLastMsgTime(toId,fromId);
    // 添加消息
    dataBase.insertMsg(fromId,toId,msg.message,msg.types);
    console.log('數(shù)據(jù)庫插入成功');
    
    // 將獲取的消息發(fā)送給好友,users[toId]就是好友的socket.id
    if(connectedUsers[toId]){
        console.log('將消息發(fā)送給',toId,'成功');
        socket.to(connectedUsers[toId]).emit('backMsg',msg,fromId,0);
    }   
});

這樣客戶端綁定backMsg事件,就能拿到發(fā)送消息了!處理消息展示即可,但需要判斷當(dāng)前用戶此時(shí)打開的聊天界面是否就是當(dāng)前發(fā)送者聊天對話框即if(fromId === this.fid && type === 0),否則會造成聊天內(nèi)容的錯(cuò)誤展示,比如當(dāng)前用戶可能存在多個(gè)好友,客戶端A給客戶端B發(fā)消息時(shí)B打開的是和C的聊天對話框,此時(shí)就會在C的對話框中錯(cuò)誤的收到A發(fā)來的消息

客戶端chatroom.vue:

this.socket.on('backMsg',(msg,fromId,type) => {
	// 如果是1對1消息fromId是當(dāng)前聊天窗口的好友id時(shí)執(zhí)行
	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;
		});
		......
	}	
});

測試效果如下: 

服務(wù)器終端輸出結(jié)果如下:

首頁新消息提示

如下圖,用戶有新消息會在首頁及時(shí)顯示,并提示未讀消息數(shù)量

需要給首頁綁定獲取消息的自定義事件backMsg,綁定時(shí)機(jī)是在生命周期onLoad中,事件一旦觸發(fā)代表有好友向你發(fā)送消息了,會獲取服務(wù)器傳來的消息,在事件回調(diào)中要完成兩個(gè)操作,首先查找發(fā)來新消息的好友在首頁好友列表數(shù)組的索引下標(biāo),隨后修改指定的數(shù)組元素內(nèi)容,更新這個(gè)好友最后消息的時(shí)間、最后消息的內(nèi)容、未讀消息數(shù);并將該元素現(xiàn)有位置刪除,添加到整個(gè)數(shù)組的頭部,即把這個(gè)好友item放到首頁列表的最上方,首頁index.vue相關(guān)代碼如下:

<view class="fl-wrapper">
	<view class="friend-list" v-for="(item,index) in friends" :key="index" @tap="toChatInterface(item)">
        <!-- 用戶頭像 -->
		<view class="avator">
            <!-- 未讀消息數(shù) -->
			<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">
                <!-- 好友名 最后聊天時(shí)間 -->
				<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:{
    // 接收個(gè)人/群聊天信息
    receiveSocket(eventName){
	    this.socket.on(eventName,(msg,fromId,type) => {
		    if(type === 0){
			    let index;
			    if(eventName == 'backMsg') {
                    // 獲取有新消息的好友在整個(gè)好友數(shù)組中的索引下標(biāo)
				    index = this.friends.findIndex((item) => {
				    	return item.id === fromId
				    });
		    	}
		    	// 修改未讀消息數(shù) 
		    	this.getUnreadMsg(this.friends[index]);
                // 修改最后聊天時(shí)間
		    	this.friends[index].lastTime = msg.time;
                // 修改最后聊天信息
		    	this.friends[index].lastMsg = msg.message;
                // 修改最后聊天信息的類型
		    	this.friends[index].lastMsgType = msg.types;
                
                // 刪除當(dāng)前item,將其插入到數(shù)組的首部,即展示在列表最上方
	    		const tempItem = this.friends[index];
	    		this.friends.splice(index,1);
	    		this.friends.unshift(tempItem);
	    	}
    	});
    },
}
 

此外還有一個(gè)問題就是何時(shí)清空未讀消息數(shù),清空的操作需要進(jìn)行兩次,一次是用戶進(jìn)入聊天頁面時(shí)進(jìn)行清空,在聊天頁生命周期onLoad中調(diào)用清空消息數(shù)的后端接口,清空現(xiàn)有的未讀消息;另一次是在點(diǎn)擊返回按鈕如下圖,返回首頁時(shí)清空,在此按鈕事件的回調(diào)中調(diào)用清空未讀消息數(shù)的接口,這是為了清空用戶和他人聊天時(shí)已讀的消息,兩次操作缺一不可。

實(shí)現(xiàn)群聊

群聊的前后端順序圖如下所示:

需要實(shí)現(xiàn)的是客戶端A在群內(nèi)發(fā)送了消息后,在同一群內(nèi)的客戶端BCD都能同時(shí)收到A發(fā)送的消息。群聊的大致思路和私聊基本相似,不同點(diǎn)在于群聊中引入了房間的概念,在房間內(nèi)的成員就是這個(gè)群聊的群成員,任何群成員的群內(nèi)發(fā)言就會在這個(gè)房間內(nèi)進(jìn)行廣播所有在線的群成員都能及時(shí)夠收到。

加入房間

使用socket.join()加入房間,具體使用如下:

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

room:房間id,是一個(gè)字符串,用戶自定義,加入房間會觸發(fā)參數(shù)二回調(diào)

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

與join相對應(yīng)的是leave方法,即退出指定的房間,參數(shù)二異?;卣{(diào)函數(shù)為可選值。需要注意的是,當(dāng)與客戶端斷開連接時(shí),會自動(dòng)將其從加入的房間中移除

在這個(gè)項(xiàng)目里房間id使用的是每一個(gè)群聊的群id號,它可以唯一標(biāo)識一個(gè)群聊;

加入房間的操作同樣是在用戶登錄成功進(jìn)入首頁時(shí)進(jìn)行,一個(gè)用戶可能加入了多個(gè)群聊,那么在主頁請求用戶群聊接口后,需要依次遍歷接口返回的群聊列表,為每一個(gè)群聊觸發(fā)addGroup事件,將當(dāng)前的群id發(fā)送給后端,讓當(dāng)前用戶加入每個(gè)群聊的房間。

index.vue

// 獲取當(dāng)前用戶的群消息
getGroup(){
	uni.request({
		url:`${this.baseUrl}/index/getGroupList`,
		method:'POST',
		data:{
			uid:this.uid,  // 用戶id
		},
		success: (res) => {
			let data = res.data.result;
            // 遍歷當(dāng)前用戶的群列表
			for (var i = 0; i < data.length; i++) {
				......
                // 觸發(fā)addGroup事件,攜帶群id,加入房間
				this.socket.emit('addGroup',data[i].id);
			}            
            ......
		}
	});
},

 服務(wù)器綁定addGroup事件,調(diào)用socket.join,讓當(dāng)前用戶連接加入房間號為groupId的房間

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

效果:例如當(dāng)前這個(gè)用戶加入了三個(gè)群聊,首頁加載后就會觸發(fā)addGroup三次,依次加入這三個(gè)群id標(biāo)識的房間。

服務(wù)器終端輸出效果如下:

發(fā)送群消息

某一群成員在群內(nèi)發(fā)送消息,會和私聊同樣的方式將語音和圖片這些靜態(tài)資源上傳服務(wù)器,返回服務(wù)器存放地址后進(jìn)行封裝,觸發(fā)gmsg事件將處理后的消息提交服務(wù)器

// 發(fā)送聊天數(shù)據(jù)
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);
	}
},

群內(nèi)廣播消息使用到的api是socket.to,具體使用如下:

將內(nèi)容發(fā)送給同在房間名roomName的所有客戶端,除了發(fā)送者

socket.to(roomName).emit('事件名',參數(shù)1,參數(shù)2,......參數(shù)n);

如果需要包含發(fā)送者可以使用

io.in(roomName).emit('事件名',參數(shù)1,參數(shù)2,......參數(shù)n);

也可以同時(shí)發(fā)送給在多間房間的客戶端,使用to鏈?zhǔn)秸{(diào)用的形式,不包含發(fā)送者

socket.to(roomName1).to(roomName2).emit('事件名',參數(shù)1,參數(shù)2,......參數(shù)n);

當(dāng)然,當(dāng)前項(xiàng)目中只需要使用第一種方式即可

服務(wù)器的gmsg事件回調(diào)中,同樣需要將獲取到的消息插入數(shù)據(jù)庫,同時(shí)修改群最后通信時(shí)間以及全體成員的未讀消息數(shù),最后調(diào)用 socket.to方法,觸發(fā)groupMsg事件,將消息發(fā)送給群聊內(nèi)的其它在線用戶。

// 引入數(shù)據(jù)庫文件
let dataBase = require("./dataBase");
// 接收群消息
socket.on('gmsg',(msg,fromId,groupId) => {
    console.log('服務(wù)器接收到來自群',groupId,'的用戶',fromId,'的消息',msg);
    // 修改群的最后通信時(shí)間
    dataBase.updateGroupLastTime(groupId);
    // 添加群消息
    dataBase.insertGroupMsg(fromId,groupId,msg.message,msg.types);
    //將所有成員的未讀消息數(shù)加一
    dataBase.changeGroupUnreadMsgNum(groupId);
    console.log('消息',msg.message,'插入數(shù)據(jù)庫成功')
    // 獲取當(dāng)前用戶的名字和頭像
    dataBase.userDetails(fromId).then((data) => {
        console.log('查詢發(fā)送者用戶名成功,用戶名是:',data[0]);
        console.log('正在將信息',msg.message,'發(fā)送至群',groupId,'內(nèi)');
        // 群內(nèi)廣播消息
        socket.to(groupId).emit('groupMsg',msg,fromId,0,data[0].name,groupId);
    });
});

客戶端在線群成員收到消息,執(zhí)行g(shù)roupMsg事件回調(diào)中的方法,內(nèi)部大致邏輯和私聊完全一致,可以將其封裝成公共方法使用,需要注意的依舊是要做群id一致性判斷,防止獲取的消息顯示在其它聊天窗口中,即 if(fromId !== this.uid && groupId === this.fid)。

this.socket.on('groupMsg',(msg,fromId,type,friendName,groupId) => {
	// 判斷當(dāng)前打開的群id和接收消息的群id是否一致,防止消息錯(cuò)誤顯示
	if(fromId !== this.uid && groupId === this.fid){
		......
		// 模擬服務(wù)器數(shù)據(jù)
		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   // 群需顯示發(fā)送消息用戶的名字
		};
		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;
		});
		......
	}	
});

效果演示:輸入一段文字發(fā)送到群內(nèi)

服務(wù)器此時(shí)終端輸出如下

以上就是項(xiàng)目聊天功能難點(diǎn)的全部內(nèi)容,前端實(shí)現(xiàn)實(shí)時(shí)聊天主要就是對于socket.io提供api的合理使用,剩余的難點(diǎn)就是頁面顯示的部分邏輯處理,用戶體驗(yàn)的優(yōu)化,還可以在此基礎(chǔ)上添加更多的功能,若有不足之處懇請指正!

到此這篇關(guān)于uni-app小程序?qū)崿F(xiàn)微信在線聊天(私聊/群聊)的文章就介紹到這了,更多相關(guān)uni-app小程序微信聊天內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論