TAT.steph 前端开发中聊天场景的体验优化
In 未分类 on 2020年04月29日 by view: 12,136
65

在最近的开发工作中,遇到了一个聊天场景的应用(Web 和小程序),类似于我们再熟悉不过的 QQ 和微信,一个正常的聊天界面是大致上是长这个样子的:


这种聊天窗口的消息流有两个明显的特点:一是最新的消息和滚动条初始位置需要在列表的最底部,二是下拉加载历史消息后要在当前消息列表的顶部进行衔接。

一般来说要实现这样的功能,对于前端开发来说都不是难事,只要两步就可以了:首先,在第一屏消息渲染完之后设置容器的 scrollTop 为一个极大值,这样就把最新消息和滚动条初始位置定位到了最底部;然后,当滚动到顶部时渲染第二屏数据,接着设置容器的 scrollTop 为衔接的位置(也就是第二屏的总高度),这样就实现了前后两屏消息的衔接。这样的 demo 只需要随手撸二三十行代码就实现了:

一开始渲染消息 1~20,滚到顶部后渲染第二屏消息 ABCDEFGHIJK,看上去前后两屏消息的衔接很平滑很流畅。目前开源社区中也有很多现成的用 React 和 Vue 开发的聊天组件或者示例,他们基本也是用上面提到的思路或者借助 iScroll 实现的。

用上面这种思路跑在 Web 中是没有任何问题的,但是在小程序中的表现却大失所望,看一下用同样的方式应用到小程序后的实际效果:

从第一段视频(左)可以看到从列表进入到聊天页面后设置滚动条位置到底部发生了明显的跳动,先看到停留在顶部然后瞬间再去到底部;第二段视频(右)滚动到顶部加载后,下一屏消息与当前消息的衔接出现了一个明显的跳动,也是先看到在顶部然后才去到预期的位置。

为什么这个思路在 Web 端体验这么好,到了小程序上体验就如此糟糕呢?原因其实很简单,这是由于小程序底层通信逻辑和视图更新机制造成的:

由于小程序跨线程通信和异步更新的特点,内容的渲染和滚动位置的设置无法保证完成的先后顺序,所以必然会先看到上一个位置一闪而过的画面。

既然是底层的问题,那么这种聊天场景在小程序中难道就玩不了了吗?当然也有尝试过用 opacity 过渡和滚动动画去缓解这种跳动,但都无法从根本上解决这两个体验问题。

当各种常规方案尝试都不尽满意的时候,那就换个思路:从本质上来说,聊天窗口的消息流实际上是一个 “反自然” 的列表,因为在计算机的 “自然界” 和人们习以为常的使用方式上,列表的初始位置都是在最顶部,想要浏览列表更多的内容需要向下滚动,而聊天场景的特点是完全反常规的!

再回到这两个体验问题:为什么需要手动设置最新消息和滚动条到最底部,为什么不让它一开始就在底部?为什么需要要在列表顶部追加数据,为什么不让它在底部追加数据?所以有没有可能颠倒常规,做一个 “反向渲染”的滚动列表呢?答案是肯定的!

首先像常规的列表一样去渲染,不需要做任何处理,第一条最新消息和滚动条的初始位置是自然地在最上面:

然后把整个列表区域的包裹容器用 CSS 旋转 180°,这样第一条最新消息和滚动条初始位置就在最下面下了:

不过此时整个列表是倒置渲染的,最后再把每一条消息组件用同样的方式旋转 180° 使它们显示回正常的视角,这样就实现了一个 “反向渲染” 的列表:

虽然是 “反向渲染”,但视觉上和正常的一模一样。此时顶部就变成了底部,向上追加数据变成了向下追加数据。最后看一下聊天列表使用 “反向渲染” 之后的体验效果:

可以看到,下拉加载消息与当前消息的衔接非常平滑没有任何的跳动,实际上这个时候历史消息是在底部渲染的,只不过反向渲染让它看上去是在顶部渲染的;此外,页面一进来最新消息和滚动条位置无需任何处理自然地停留在最底部,接近原生体验。

这种 “反向渲染” 的思路用最少的代码就解决了消息场景在小程序上这种几乎无解的问题,并且达到了最优的体验,而实际上核心代码只有两行 CSS:

整个过程无需任何手动设置滚动位置和计算第二屏总高度(实际上都不用关心它们了),同样这种思路用在 Web 上也是 OK 的。当然用了反向渲染也有一些牺牲,比如 iOS 双击顶部栏回到顶部这个特点就不能用了,但总体来说获得体验上的优化是更多的。

此外,聊天场景中的消息流通常也有这样的布局:

如果视觉上需要将自己和别人的消息方向分别位列两边对齐,那么利用这种 “反向渲染” 的思路,实现起来也非常容易,只需要对消息组件应用不同的 CSS 样式即可:

消息流的每一条消息都是一个单独的组件,此时不需要为了区分不同的视角而去新写一个组件,也不需要改变现有组件的结构布局。

原创文章转载请注明:

转载自AlloyTeam:http://www.alloyteam.com/2020/04/14349/

  1. sleep 2021 年 4 月 28 日

    设置容器的 scrollTop 为衔接的位置,就是第二屏的总高度,这样做不会出现闪动么,因为消息的内容是不固定的,需要加载出来后才能获取到总高度,然后拿这个高度给 scrollTop 添加.

    • TAT.steph

      TAT.steph 2021 年 4 月 29 日

      如果不固定肯定会闪动的,特别是有图片网络加载的情况下,这样的话只能在列表页显示固定的高度,比如九宫格布局,又或者后台存一下图片的宽高,加载之前就占据所需的大小。

      • sleep 2021 年 4 月 29 日

        我用的是 react, 这个在移动端安卓上没有闪烁效果,ios 上有闪烁,就是很轻微的闪烁,现在尝试了你的翻转列表的方式

  2. Jack 2021 年 3 月 9 日

    It seems that doesn’t work on ios ?

    • TAT.steph

      TAT.steph 2021 年 4 月 29 日

      How about the detail?

  3. louiebb 2021 年 1 月 26 日

    既然是 “反自然” 的列表,为何不从后端数据的返回入手?transform: rotate(180deg); 会有触发重排性能问题,让后端改一下返回顺序不就完美解决?

    • TAT.steph

      TAT.steph 2021 年 1 月 26 日

      本文所描述的和数据无关

  4. Tron1234 2021 年 1 月 18 日

    你好今天又发现这个方法还有个弊端,导致 scroll-into-view 无效,现在设置 id 无法跳转到指定位置

    • Tron1234 2021 年 1 月 18 日

      只能自己通过控制 scrollTop 进行定位

    • john 2021 年 4 月 17 日

      我也发下了该问题,总是滚动到顶部 (视觉上

  5. Tron1234 2021 年 1 月 12 日

    发现一个有意思的问题,如果接收到一条新消息则整个列表会向上顶,这是正常的,但是像微信滚动到一定位置之后,收到一条新消息整个列表不会向上顶,保持当前滚动的位置,这个请问是否有办法实现,利用 scrollTop 重新设置定位效果不太好

    • TAT.steph

      TAT.steph 2021 年 1 月 12 日

      试试 overflow-anchor 能不能解决。

      • Tron1234 2021 年 1 月 12 日

        谢谢指点,已经试过了没有效果,而且这个 css 兼容性特别差,目前的做法是监听列表高度变化,然后设置 scrollTop 效果不理想,所以想问问有什么高见。这个发布评论审核时间太长了

        • TAT.steph

          TAT.steph 2021 年 1 月 13 日

          如果这个 CSS 无法解决的话,动态设置 scrollTop 理论上是可以做到的(小程序可能效果不行,web 肯定是可以的,计算准确就行),如果这两者试过都不理想,那么只能从交互实现上做一些妥协,比如判断当滚动条位置不在最底下时,来的新消息先不渲染到列表里面,点击回到新消息提醒浮层后再更新到列表,然后 scrollTop = 0 回到最底下。

          • Tron1234 2021 年 1 月 13 日

            谢谢您的建议,我也考虑过后面再添加收到的新消息,但如果当收到一条新消息,用户手动滑动到最底,那么在滑动的时候添加信息到列表,也会感觉 dom 有明显的增加,如果是到最底的时候在添加,一条新消息效果比较理想,如果是多条消息的话会突然多出很多条也会感觉很奇怪,目前没想到更好的方法能保持不滚动并且 dom 还能增加

  6. Tron1234 2021 年 1 月 11 日

    请问一下有没有出现 scroll-view 到顶或底然后无法滚动的情况

    • TAT.steph

      TAT.steph 2021 年 1 月 11 日

      没有,可能是遇到 iOS 滚动穿越了

      • Tron1234 2021 年 1 月 12 日

        感谢回答,请问一下那个加载完成后滚动位置保持不变,下一页的内容出现在加载中的那片区域是怎么实现的,麻烦给个思路,谢谢

        • Tron1234 2021 年 1 月 12 日

          这个问题刚刚自行解决了

      • Tron1234 2021 年 1 月 12 日

        android 也有这个问题,scroll-view 正向滚动没有这个问题,transform:rotate(180deg)就存在到顶卡死问题

      • Tron1234 2021 年 1 月 12 日

        刚刚写了个 demo,发现应该是我自己布局导致卡死的问题

  7. 来吧 2021 年 1 月 3 日

    大哥,现在遇到一个问题,例如:在当前聊天页,获取了很多历史聊天记录,这个时候来了一条信息,消息就往下移动一点距离,这个问题怎么解决呀?我用的是 vue 写的 web.

    • 来吧 2021 年 1 月 3 日

      不好意思大哥, 已经解决了。谢谢

      • he 2021 年 6 月 11 日

        请问下怎么解决的

  8. john 2020 年 12 月 25 日

    有这样的情况吗,鼠标和界面的交互反了?正常交互的话: 鼠标往下,页面向上滚动,但是按照这个思路这个交互就反过来了

    • john 2020 年 12 月 25 日

      也就是鼠标往下滑,页面往下滚动

    • TAT.steph

      TAT.steph 2020 年 12 月 25 日

      已在其他评论回复这个问题,可以翻一下,有办法翻转鼠标滚动方向的。

  9. 我是小黄啊 2020 年 9 月 26 日

    你好, 请教一下这种方法 scroll-view 组件的高度是设置固定还是 100vh? 如果不设置高度的话,scroll-top 属性就不能启用了。 谢谢

    • 我是小黄啊 2020 年 9 月 26 日

      我已经在评论里看到了类似问题,我还想问一下,这个 scroll-view 的 height 如何设置成动态的? 谢谢

      • TAT.steph

        TAT.steph 2020 年 9 月 27 日

        获取设备高度 + 监听键盘弹出就行了。

  10. lance 2020 年 8 月 10 日

    transform rotate(180deg) 之后,貌似文字会出现模糊的现象,不知道是否遇到过,或者是怎么解决的?加了 translateZ 之后,部分屏幕下能好,部分屏幕下模糊

    • TAT.steph

      TAT.steph 2020 年 8 月 10 日

      旋转 180 度导致字体模糊?这个倒没遇到过。

  11. zj 2020 年 7 月 31 日

    这里有个问题,当聊天数据很少(只有一两条)的时候,依然是从底部开始向上排列的,而正常的聊天界面始终是从上到下排列的,这个问题如何解决。

    • TAT.steph

      TAT.steph 2020 年 8 月 2 日

      这个问题实践中也遇到过,解决方案是只需要确保滚动区域的容器的最小高度大于等于整个列表的外容器高度即可,不可以让它取决于一两条消息的高度,通常可以设置成父容器的 100% 或者动态计算手机屏幕大小。

      • zj 2020 年 8 月 3 日

        感谢回答,如果只是将滚动区域的最小高度设置为 100% 的情况下排列还是从下至上的。
        现在我的解决办法是在 scrollview 下加一层 view, 将 view 的 min-height 设置为 100%。
        然后 display:flex;flex-direction: column;justify-content: flex-end。

        整体结构是 scroll-view -> view(min-height:100%) -> [chat-item] 列表

        • Tron1234 2021 年 1 月 12 日

          俺也一样

        • ken 2022 年 7 月 19 日

          我也遇到这个问题了,按照你写的试了一下,果然没问题了

  12. 路人乙 2020 年 7 月 31 日

    貌似存在一个问题就是,如果消息列表不能占满这个屏的话,默认是从下至上的展现形式。正常的一般是从上至下。

    • TAT.steph

      TAT.steph 2020 年 8 月 2 日

      已在其他评论回复。

  13. 呀哈哈 2020 年 7 月 28 日

    如果历史消息不足一屏的话,这样倒过来不就很奇怪了

  14. 大颂颂 2020 年 7 月 28 日

    display: flex;flex-flow: column-reverse;

    • TAT.steph

      TAT.steph 2020 年 7 月 28 日

      这样只是每个消息渲染顺序颠倒,滚动条初始位置还是在最上面,其实用不用 reverse 都没区别。

  15. LFQ 2020 年 6 月 19 日

    思路真好

  16. cat 2020 年 5 月 20 日

    我按照要求将 包裹器 rotate 180deg 了 但是 滚动条依旧在最顶部 貌似不起效果, 该如何解决呢 (微信开发者工具)

    • cat 2020 年 5 月 20 日

      已经解决了 还一点 很重要, 包裹器必须设定固定高度, 不然也不会生效

      • TAT.steph

        TAT.steph 2020 年 5 月 20 日

        总之是需要把列表区域旋转,几层包裹器没所谓的。

        • cat 2020 年 5 月 25 日

          的确, 这个旋转的这个思路想法很独特, get 到了

        • cat 2020 年 5 月 25 日

          还有一个问题想请教下, 该布局应用到 web 端的时候会出现 滚轮滚动反向滚动, 这样的话 有什么好的解决方法妈

          • TAT.steph

            TAT.steph 2020 年 5 月 25 日

            这个方法比较适用于移动端滚动,PC 的鼠标滚轮确实会反向,但是可以通过 mousewheel 事件去把滚动方向再反一次,stackoverflow 上有的你可以搜一下。

      • 半逐梦 2022 年 6 月 2 日

        这个问题我也遇到了,包裹器旋转 180deg 后只是渲染顺序颠倒了,但是滚动条依然在顶部,请问你是怎么解决的呢?

  17. kevin198 2020 年 5 月 18 日

    滚动条本来在右边,我的列表 List 按倒序排列之后 再渲染,旋转 180 度之后,新消息是到底部了,可以滚动条不是相应的就到了左边么? 怎么你的效果图,旋转 180 度后,滚动条居然还在右侧呢?而且旋转之后,滚动效果居然消失了,无法滚动

    • TAT.steph

      TAT.steph 2020 年 5 月 19 日

      用 CSS direction 属性控制。

    • 呀哈哈 2020 年 7 月 28 日

      直接 rotateX(180deg) 就行了吧

  18. Twinkle 2020 年 5 月 6 日

    按照图片显示的旋转方式,容器和列表元素都应该是 rotateY 吧。

  19. 路人甲的世界 2020 年 5 月 2 日

    这让我想起了另一个 CSS Trick:鼠标滚轮上下滚动实现 div 的左右滚动,在早期的一些页面(尤其是仿 Windows8 风格的)很常用,只不过这个是旋转 180 度,然后元素旋转-180 度,那个是旋转 90 度,然后元素旋转-90 度~

  20. 白晓寒 2020 年 4 月 30 日

    直接隐藏这个 div
    然后滚动到底部再显示 div….

    • TAT.steph

      TAT.steph 2020 年 4 月 30 日

      这样做从隐藏到显示同样会存在一刻白屏。

发表评论