前言

昨天,在我的开源项目chat-system中查看聊天记录时,发现消息中如果有图片滚动条的位置就会算错,导致最后一条消息定位不准确。

经过一番排查后,终于解决了这个问题,本文就跟大家分享下我的解决方案与思路,欢迎各位感兴趣的开发者阅读本文。

问题分析

如下图所示,我们点开一个聊天窗口,最后一条消息是图片,滚动条位置计算有误,没有触底,导致图片没有显示完全,在上拉加载历史消息时也是因为图片导致的滚动条位置计算失误,没有正确定位到上次浏览的消息位置。

滚动条触底分析

我们来看下触底时滚动条位置计算的实现代码:

nextTick().then(() => { let scrollHeight = 0; if (messagesContainer.value == null) return; // 获取消息容器滚动区域高度 scrollHeight = messagesContainer.value.scrollHeight; // 当前滚动条在底部或者当前消息为发送端所发送的则修改滚动条位置 if (isBottomOut.value || data.isSendMessages.value) { // 新消息渲染完成,修改滚动条位置 messagesContainer.value.scrollTop = scrollHeight; } });

如上述代码所示,我们在nextTick回调中获取了消息容器的滚动区域高度,然后修改滚动条位置为滚动区域高度,这样滚动条就会触底了,逻辑上没问题,而且在纯文字的消息中是正常的。

那么,问题可能出在获取消息容器高度时,没有获取正确,于是我尝试了下将scrollHeight改为99999,这样它的滚动条就肯定在底部了。

然而,并没有我预想的那样顺利,改成99999后,滚动条的位置依然是错的。

那么,我想问题应该是nextTick()后滚动条确实到底部了,但是此时图片还没有加载完,图片加载完成后滚动条位置就又变了。

此时,我们就找到了问题,那么我们就可以得到下述解决思路:

  • 获取页面内的所有聊天图片
  • 遍历获取到的图片
  • 每一张图片加载完成后就获取可滚动容器的高度,然后修改滚动条位置

滚动条触顶分析

触顶加载数据时,也是因为图片的缘故,导致了滚动条位置计算失误,一开始我选择沿用触底的时的方案,等img加载完成后获取滚动容器的高度,然后用当前消息容器高度 - 上一次保存的消息容器高度,这样就能计算出上一次浏览消息时的滚动条位置。

按照上述思路实现后,滚动条的位置依然是错的,经过一番调试后,发现每次触顶时,dom都会重新加载,自然已经加载过的图片还会重新加载一次,滚动条的位置自然也就算错了。

经过一番思考后,我想到了一个解决方案,既然等图片加载完行不通,那我就用定时器吧。

  • nextTick()后,等待150ms,然后获取消息容器的可滚动高度.
  • 计算滚动条的位置
  • 修改滚动条位置

实现代码

接下来,我们来看下具体的实现代码。

滚动条触底

滚动条触底时的部分代码如下所示,完整代码请移步:messageParsing.ts

nextTick().then(() => { const scrollHeight = 0; // 获取页面内所有的聊天图片 const previewablePanel = document.getElementsByClassName("previewable"); if (messagesContainer.value == null) return; for (let i = 0; i < previewablePanel.length; i++) { const item = previewablePanel.item(i) as HTMLImageElement; item.onload = () => { if (messagesContainer.value == null) return; // 置底滚动条 bottomScrollBar( scrollHeight, messagesContainer as Ref<HTMLDivElement>, isBottomOut, msgListPanelHeight, isFirstLoading ); }; } });
const bottomScrollBar = ( scrollHeight: number, messagesContainer: Ref<HTMLDivElement>, isBottomOut: Ref<boolean>, msgListPanelHeight: Ref<number>, isFirstLoading: Ref<boolean> ) => { const data = initData(); // 显示消息内容 data.msgShowStatus.value = ""; // 获取容器高度 scrollHeight = messagesContainer.value.scrollHeight; // 当前滚动条在底部或者当前消息为发送端所发送的则修改滚动条位置 if (isBottomOut.value || data.isSendMessages.value) { // 新消息渲染完成,修改滚动条位置 messagesContainer.value.scrollTop = scrollHeight; // 更新消息记录容器高度 msgListPanelHeight.value = scrollHeight; // 修改组件第一次加载状态为false isFirstLoading.value = false; // 修改消息发送端状态为false data.isSendMessages.value = false; } };

滚动条触顶

滚动条触顶时的部分代码如下所示,完整代码请移步:messageParsing.ts

nextTick().then(() => { // 隐藏消息内容 data.msgShowStatus.value = "hidden"; if (data.pageNo.value > 20) { // 数据加载超过20条,加载时间改为400ms loadingTime = 400; } setTimeout(() => { if (messagesContainer.value == null) return; scrollHeight = messagesContainer.value.scrollHeight; // 加载历史消息,修改滚动条位置:当前消息记录容器高度 - 消息记录容器高度 messagesContainer.value.scrollTop = scrollHeight - msgListPanelHeight.value; // 一条消息渲染完成,待渲染消息总条数自减 msgTotals.value--; // 判断消息是否渲染完成 if (msgTotals.value === 0) { // 显示消息内容 data.msgShowStatus.value = ""; // 关闭加载动画 isLoading.value = false; // 加载历史消息完成,更新消息记录容器高度 msgListPanelHeight.value = scrollHeight; } }, loadingTime); });

在上述代码中,定时器的时间是动态的,是因为我发现当加载的消息超过20页时,等待150ms已经拿不到正确的可滚动容器高度了,需要等待400ms。

实现效果

接下来,我们来看下最终的实现效果。

滚动条触顶

在上述实现代码中,我还做了一个优化,nextTick后我隐藏了消息内容,滚动条位置计算完成后,让消息内容再显示出来。

至于为什么要做这个优化,我通过gif图来描述下吧,我们先来看下没做优化时的触顶加载效果,如下所示:

如上图所示,未优化时加载消息会先闪一下错误位置的消息,然后才会展示正确的消息,看着很难受。

接下来,我们来看下优化后的效果,如下所示:

优化后,视觉效果相比未优化时要好上很多,虽然还是有点瑕疵,会闪烁一下?,目前想不到其他解决方案了,只能先这样了,如果大家有更好的方案,可在评论区一起讨论。

滚动条触底

滚动条触底时,由于是需要等图片加载完成后修改滚动条的位置,图片未加载完成时,界面会先闪一下错误位置的消息,然后才是正确的消息。

触底时,我采用了与触顶时相同的解决方案,滚动条位置计算完成后才让聊天记录显示,实现效果如下所示:

项目地址

写在最后

  • 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注?
  • 本文首发于掘金,未经许可禁止转载?