uniapp聊天室实现触顶加载更多历史数据

851 阅读3分钟

前言

最近因为项目需要,做了一个即时通讯的聊天室模块。

当我做到加载更多历史数据功能时发现。每次向顶部添加数据时,滚动条都会回到顶部的问题。

情况如下(以下是模拟项目中的效果)

freecompress-Demo1.gif

可以看到数据加载完后滚动条位置并不会保持在滚动前最后一条消息的上方,而是在加载回的数据。

分析

在每次触顶的时候,滚动条的scrollTop为0,当数据加载回来并渲染上页面时,scrollTop依旧没有改变,依然保持在顶部。

解决方案

1.动态设置scrollTop

实现思路:

为每一条数据设置一个id。当触顶加载时,记录下当前消息列表中最顶部的数据的id,数据加载完后,获取这个id数据所对应节点的信息,将scroll组件的scroll-top属性值设置为该节点的最新top,则可以达到触顶时加载更多数据时,保持在最初位置的效果。

<template>
	<view class="container">
		<view class="main">
			<scroll-view
				class="list"
				scroll-y
				enable-flex
				:scroll-top="scrollTop"
				@scroll="handleScroll"
				@scrolltoupper="handleScrollToupper"
				id="scroll"
			>
				<view 
					class="list__item" 
					:class="{ 'list__item--right': item.flow == 'OUT'  }" 
					v-for="(item, index) in list"
					:id="'item__' + item.id"
				>
					<view>
						<image 
							mode="aspectFill" 
							class="list__item--image"
							:src="getUserAvater(item)"
						/>
					</view>
					<view class="list__item__message">
						<text class="list__item__message--text">
							{{ item.message }}
						</text>
					</view>
				</view>
			</scroll-view>
		</view>
		<view class="footer"></view>
	</view>
</template>

<script>
	export default {
		name: 'ScrollTopPage',
		onLoad() {
			MessageData.forEach(item => {
				item.id = randomString(16)
			})
			this.list = MessageData
		},
		data() {
			return {
				list: [],
				triggeringTimeId: '',
				scrollTop: 0,
				oldScrollTop: 0
			};
		},
		computed: {
			getUserAvater() {
				return function(item) {
					return item.flow == 'OUT' ? Test1Image : Test2Image
				}
			}
		},
		methods: {
			// 触顶部
			handleScrollToupper() {
				uni.showLoading({
					title: '加载中...',
				})
				this.triggeringTimeId = this.list[0].id
				setTimeout(() => {
					for(let i = 0; i <= 10; i++) {
						const item = {
							flow: 'OUT',
							message: randomString(30)
						}
						this.list.unshift(item)
					}
					this.scrollTriggeringTimeEle()
					uni.hideLoading()
				}, 2000)
			},
			// 滚动至触发时的item top
			scrollTriggeringTimeEle() {
				// 获取列表渲染完毕后,旧元素的top
				this.$nextTick(() => {
					const query = uni.createSelectorQuery();
					query.select(`#item__${this.triggeringTimeId}`).boundingClientRect((data) => {
					    console.log('data', data.top)
						this.scrollTop = data.top - 32
					}).exec();
				})
			},
			handleScroll(e) {
				this.oldScrollTop = e.detail.scrollTop
			}
		}
	}
</script>

缺点:

  1. 这里演示的是H5端,在不同的端上有不同的效果。如在APP和小程序上时可能会出现闪烁
  2. 若加载的聊天消息中包含图片信息,当图片未能完全加载回来时,可能会导致节点的top位置不准确。

2.翻转scroll组件

实现思路:

将scroll组件进行一个180度的翻转,使scroll组件的底部位于页面的上方,顶部位于页面的下方。则此时最新的消息应在页面的顶部,而不是在页面的底部。这样一来,当我们加载更多历史数据时,直接向scroll组件的底部追加数据后,顶部的位置就不会跑到加载的数据里了。

freecompress-Demo3.gif

<template>
	<view class="container">
		<view class="main">
			<scroll-view
				class="list"
				scroll-y
				enable-flex
				@scrolltolower="handleScrollTolower"
			>
				<view class="list__item" :class="{ 'list__item--right': item.flow == 'OUT'  }" v-for="item in list">
					<view>
						<image 
							mode="aspectFill" 
							class="list__item--image"
							:src="getUserAvater(item)"
						/>
					</view>
					<view class="list__item__message">
						<text class="list__item__message--text">
							{{ item.message }}
						</text>
					</view>
				</view>
			</scroll-view>
		</view>
		<view class="footer"></view>
	</view>
</template>

<script>
	export default {
		name: 'ScrollReversePage',
		onLoad() {
			this.list = MessageData.reverse()
		},
		data() {
			return {
				list: []
			};
		},
		computed: {
			getUserAvater() {
				return function(item) {
					return item.flow == 'OUT' ? Test1Image : Test2Image
				}
			}
		},
		methods: {
			// 触底
			handleScrollTolower() {
				uni.showLoading({
					title: '加载中...',
				})
				setTimeout(() => {
					for(let i = 0; i <= 10; i++) {
						const item = {
							flow: 'OUT',
							message: randomString(30)
						}
						this.list.push(item)
					}
					uni.hideLoading()
				}, 2000)
			}
		}
	}
</script>

<style lang="scss" scoped>
	page {
		width: 100%;
		height: 100%;
		background-color: #f5f5f5;
	}
	.container {
		display: flex;
		flex-direction: column;
		width: 100%;
		height: 100%;
	}
	.main {
		height: calc(100% - 100rpx);
		display: flex;
		flex-direction: column;
		/deep/.uni-scroll-view-content {
			display: flex;
			flex-direction: column;
		}
		.list {
			box-sizing: border-box;
			flex: 1;
			display: flex;
			flex-direction: column;
			overflow: scroll;
			padding: 0 32rpx;
			transform: rotateX(180deg);
			&__item {
				margin-top: 24rpx;
				display: flex;
				transform: rotateX(-180deg);
				&--right {
					flex-direction: row-reverse;
					margin-right: 64rpx;
				}
				&--image {
					width: 72rpx;
					height: 72rpx;
					border-radius: 100%;
				}
				&__message {
					display: flex;
					align-items: center;
					max-width: 64%;
					background-color: #5898F8;
					padding: 24rpx;
					margin: 0 24rpx;
					border-radius: 16rpx;
					color: #FFFFFF;
					overflow: hidden;
					&--text {
						word-break:break-all;
						font-size: 30rpx;
					}
				}
			}
		}
	}
	.footer {
		width: 100%;
		height: 100rpx;
		background-color: #FFFFFF;
	}
</style>

以下是demo的地址:

Demo2页面为案例一

Demo3页面为案例二

gitee.com/xie-ruifeng…

写的不好,请各位大佬多多指教...