之前学习使用uni-app简单实现一个在线聊天的功能,今天记录一下项目核心功能的实现过程。页面UI以及功能逻辑全部来源于微信,即时聊天业务的实现使用socket.io,前端使用uni-app开发,后端服务器基于node实现,数据库选择mongoDB。
首先在系统中注册两个用户,将对方添加为好友后,开始正常聊天,先简单看一下聊天功能的效果图,分为私聊和群聊两大部分
一对一聊天效果:
在好友列表中添加群成员创建群后即可群聊,群聊效果:
目录
聊天信息列表的渲染
聊天信息列表区域是一个滚动区,这里使用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