在最近的开发工作中,遇到了一个聊天场景的应用(Web 和小程序),类似于我们再熟悉不过的 QQ 和微信,一个正常的聊天界面是大致上是长这个样子的:
这种聊天窗口的消息流有两个明显的特点:一是最新的消息和滚动条初始位置需要在列表的最底部,二是下拉加载历史消息后要在当前消息列表的顶部进行衔接。
一般来说要实现这样的功能,对于前端开发来说都不是难事,只要两步就可以了:首先,在第一屏消息渲染完之后设置容器的 scrollTop
为一个极大值,这样就把最新消息和滚动条初始位置定位到了最底部;然后,当滚动到顶部时渲染第二屏数据,接着设置容器的 scrollTop
为衔接的位置(也就是第二屏的总高度),这样就实现了前后两屏消息的衔接。这样的 demo 只需要随手撸二三十行代码就实现了:
一开始渲染消息 1~20,滚到顶部后渲染第二屏消息 ABCDEFGHIJK,看上去前后两屏消息的衔接很平滑很流畅。目前开源社区中也有很多现成的用 React 和 Vue 开发的聊天组件或者示例,他们基本也是用上面提到的思路或者借助 iScroll 实现的。
用上面这种思路跑在 Web 中是没有任何问题的,但是在小程序中的表现却大失所望,看一下用同样的方式应用到小程序后的实际效果:
从第一段视频(左)可以看到从列表进入到聊天页面后设置滚动条位置到底部发生了明显的跳动,先看到停留在顶部然后瞬间再去到底部;第二段视频(右)滚动到顶部加载后,下一屏消息与当前消息的衔接出现了一个明显的跳动,也是先看到在顶部然后才去到预期的位置。
为什么这个思路在 Web 端体验这么好,到了小程序上体验就如此糟糕呢?原因其实很简单,这是由于小程序底层通信逻辑和视图更新机制造成的:
由于小程序跨线程通信和异步更新的特点,内容的渲染和滚动位置的设置无法保证完成的先后顺序,所以必然会先看到上一个位置一闪而过的画面。
既然是底层的问题,那么这种聊天场景在小程序中难道就玩不了了吗?当然也有尝试过用 opacity
过渡和滚动动画去缓解这种跳动,但都无法从根本上解决这两个体验问题。
当各种常规方案尝试都不尽满意的时候,那就换个思路:从本质上来说,聊天窗口的消息流实际上是一个 “反自然” 的列表,因为在计算机的 “自然界” 和人们习以为常的使用方式上,列表的初始位置都是在最顶部,想要浏览列表更多的内容需要向下滚动,而聊天场景的特点是完全反常规的!
再回到这两个体验问题:为什么需要手动设置最新消息和滚动条到最底部,为什么不让它一开始就在底部?为什么需要要在列表顶部追加数据,为什么不让它在底部追加数据?所以有没有可能颠倒常规,做一个 “反向渲染”的滚动列表呢?答案是肯定的!
首先像常规的列表一样去渲染,不需要做任何处理,第一条最新消息和滚动条的初始位置是自然地在最上面:
然后把整个列表区域的包裹容器用 CSS 旋转 180°,这样第一条最新消息和滚动条初始位置就在最下面下了:
不过此时整个列表是倒置渲染的,最后再把每一条消息组件用同样的方式旋转 180° 使它们显示回正常的视角,这样就实现了一个 “反向渲染” 的列表:
虽然是 “反向渲染”,但视觉上和正常的一模一样。此时顶部就变成了底部,向上追加数据变成了向下追加数据。最后看一下聊天列表使用 “反向渲染” 之后的体验效果:
可以看到,下拉加载消息与当前消息的衔接非常平滑没有任何的跳动,实际上这个时候历史消息是在底部渲染的,只不过反向渲染让它看上去是在顶部渲染的;此外,页面一进来最新消息和滚动条位置无需任何处理自然地停留在最底部,接近原生体验。
这种 “反向渲染” 的思路用最少的代码就解决了消息场景在小程序上这种几乎无解的问题,并且达到了最优的体验,而实际上核心代码只有两行 CSS:
1 2 |
transform: rotate(180deg); direction: rtl; |
整个过程无需任何手动设置滚动位置和计算第二屏总高度(实际上都不用关心它们了),同样这种思路用在 Web 上也是 OK 的。当然用了反向渲染也有一些牺牲,比如 iOS 双击顶部栏回到顶部这个特点就不能用了,但总体来说获得体验上的优化是更多的。
此外,聊天场景中的消息流通常也有这样的布局:
如果视觉上需要将自己和别人的消息方向分别位列两边对齐,那么利用这种 “反向渲染” 的思路,实现起来也非常容易,只需要对消息组件应用不同的 CSS 样式即可:
消息流的每一条消息都是一个单独的组件,此时不需要为了区分不同的视角而去新写一个组件,也不需要改变现有组件的结构布局。
伟子 2022 年 6 月 11 日
这么做其实还是有问题 如果是群聊的情况下 用户正在查看历史消息的同时碰巧接收了新消息 用户会发现消息列表在一直向上跳动
TAT.steph 2022 年 7 月 19 日
增加一些标记位判断应该也是可以解决的,这种情况就复杂些
w 2021 年 12 月 29 日
大佬请教一下,您聊天界面的下拉刷新咋做的,用到了哪些事件监听,我想用 touchmove,但是实际上只有 touchstart 和 touchend 会触发
sleep 2021 年 5 月 17 日
大佬,帖子可以转发么
采用了这个方法后,需求已经完成测试成功上线了,效果也很好达到了产品的要求, 想把这个方法分享给大家
TAT.steph 2021 年 5 月 17 日
可以,请务必注明出处和链接到 AlloyTeam 博客。
sleep 2021 年 5 月 18 日
OK
sleep 2021 年 4 月 29 日
按照你的方法做了一下处理,效果实现了,就是安卓上有一个问题,滑动的方向没有转过来,手指向上滑动,列表变为向下滑动了,但是 ios 上效果正常,和正常的滑动效果一样,在网上找了好久没有找到解决办法
sleep 2021 年 4 月 29 日
我知道了。。。。找安卓一起调了,发现是 url 配置了开启 eruda,eruda 和这个方法有冲突。去掉了 eruda 就正常了,不知道 eruda 内部做了什么处理竟然能影响到这个。