最近在使用uniapp开发一个群聊的小程序,需要加载聊天历史的功能,因此使用scroll-view实现,但这个过程可谓是非常坎坷。
作为一个后端开发,本来就对前端的什么渲染,promise不太懂,所以总是在代码的执行时序上出问题,比如nextTick的使用时机,因此也犯了很多低级错误。
与传统的在底部加载更多不同,聊天历史需要在容器顶部追加数据,这就导致了一个问题,数据加载完成后会跳到容器顶部,我期望的是它能停留在当前位置,解决方案有以下
基本结构
<scroll-view
scroll-anchoring
:scroll-top="amend"
@scrolltoupper="scrolltoupper"
:scroll-into-view="tagLocation"
:upper-threshold="upperThreshold"
class="scroll-view"
scroll-y
id="scroll-view"
>
<view class="flex flex-col-reverse" id="chat-content">
<view v-for="(item, index) in chatRecord" :key="index" :id="`item-${item.id}`">
</view>
</view>
</scroll-view>
1. 给每个消息标签添加id,当数据渲染完成后,使用scroll-into-view定位,但这会导致页面闪烁,原因是数据渲染后先跳到容器顶部,然后又拉到scroll-into-view标记的id位置,页面渲染了两次。
2. 在请求数据前,记录内容盒子高度,数据渲染后,再查看内容盒子高度,然后两者相减,就是scroll-top的值,但效果和1一样,甚至不如1
3. 滚动容器上下反转,内容上下反转,如果有滚动条,再左右镜像。这种做法相当于传统的底部加载更多功能,数据渲染到容器的底部,数据渲染不会导致当前位置改变,但这种做法也有问题,鼠标滚轮是反向的,对pc用户不友好。
4. 官方给出了使用css overflow-anchor:auto;的方案。一开始使用了,发现不生效,于是乎没注意它,后来又详细了解了overflow-anchor:auto;这个功能,发现它在web端确实有用,既然官方提到了它,说明它在小程序是适用的,肯定是我的用法不对。如下
- ① 使用overflow-anchor:auto;在完成数据渲染后,不要再使用定位,也就是不要使用scroll-top和scroll-into-view属性,否则又回到了1、2的情况
- ② 当内容滑到了容器顶部,也就是滑到不能再往上滑了,在完成数据渲染后,会跳到容器顶部,overflow-anchor:auto;不生效,原因未知。
- ③ 新数据追加到数组的首部无效的话,可以试试将新数据添加到数组的尾部,这样不会改变数组旧数据的索引。然后内容盒子添加display: flex; 和flex-direction: column-reverse; 让数据从下到上展示(当然接口的数据也应该反过来)
通过测试,如果避免以上情况,数据可以做到无感加载,类似于微信加载历史一样。所以要解决的问题是,如何做到内容滑到顶部之前把数据渲染出来,或者如何防止内容滑到顶部。
当你滑动微信记录时,你会发现,微信的滑动被限速了,猜测是为了给加载数据提供时间。但是小程序好像没有能限制滑动速度的方法。此路不通
我们无法避免内容被滑到顶部,特别在小程序里,加载历史是网络请求,时间无法预估。所以应该在数据渲染之前,判断当前是否滑到顶部,如果滑到顶部,向下微调1个像素,这样渲染后就不会跳到容器顶部了,
//局部代码------------------------------------------------
//data.top表示容器视口顶部位于屏幕的y坐标
const query = uni.createSelectorQuery().in(instance.proxy);
const all = Promise.all([
new Promise((resolve) => {
query
.select('#scroll-view')
.boundingClientRect((data) => {
resolve(data.top)
})
.exec()
}),
new Promise((resolve) => {
query
.select('#chat-content')
.boundingClientRect((data) => {
resolve(data.top)
})
.exec()
}),
])
//滚动容器的top值是恒定不变的,内容盒子的top随着滑动而改变,
//当内容盒子滑到顶部时,二者相等,滚到顶部时overflow-anchor:auto;失效。
//所以在渲染数据前,手动往下滚动1个像素微调一下,
//也就是 :scroll-top = amend 这里的随机数不可去掉,等到微调完毕再渲染数据
all.then((topArr) => {
let dif = Math.abs(topArr[0] - topArr[1])
if (dif == 0) {
amend.value = Math.random() / 2 + 1
}
nextTick(() => {
//微调完毕页面刷新后再更新数据,否则不生效
update() //向数组中加数据
})
})
其实这个方案也是不完美的,
①如果用户滑到顶部,但不松手,此时容器的top == 内容的top,且无法通过amend修正,也就是 :scroll-top = amend是不生效的,我试图在渲染数据时阻断用户触摸,但没找到对应的api,即使在请求数据时添加蒙层也不行,蒙层只能阻止蒙层触发后的触摸,没法切断蒙层触发之前的触摸。这种情况把触顶阈值设大点一般不会出现,除非接口特别慢。
②如果滑的非常快,接口请求也非常快,偶尔也会导致overflow-anchor:auto;失效, 我猜测在判断是否滑到顶部时为false,也就是if (dif == 0)时是false,但渲染时变成true了,因为滑的太快了。这里只能通过限制滑动速度解决,像微信那样,然而我没有找到这种api。退而求其次,就是尽量让一次请求的数据多一些,然后触顶事件的阈值大一些,尽量在滑到顶部之前就渲染完新的数据。也就是upper-threshold设的大点,我的upper-threshold设的动态值,会计算新渲染的数据的高度,然后乘以一个百分数,比如设的0.8,这样在滑动新数据的20%时我就开始请求下一页数据了。
const selectorQuery = (fun) => {
const query = uni.createSelectorQuery().in(instance.proxy)
query
.select('#chat-content')
.boundingClientRect((data) => {
fun(data)
})
.exec()
}
//触顶事件
let just = 0 //请求数据之前的总高度
const upperThreshold = ref(200) //绑定:upper-threshold
const loadHistory = async () => {
selectorQuery(async (data) => {
just = data.height
//网络请求中。。。。。。。
await queryHistory(......) //这里会把新的数据追加到列表数组里
nextTick(() => {
selectorQuery((data) => {
//新数据的高度 = 新数据渲染完后的总高度 - 之前的高度, 再乘以0.8是触发触顶事件的阈值
let res = (data.height - just) * 0.8
if (res < 1) {
res = 1
}
upperThreshold.value = res
})
})
})
}
最终实现的效果可以查看微信小程序: 一键财税甩单