TAT.steph 前端开发中聊天场景的体验优化
In Web开发 on 2020年04月29日 by view: 1,449
24

在最近的开发工作中,遇到了一个聊天场景的应用(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. lance 2020 年 8 月 10 日

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

    • TAT.steph

      TAT.steph 2020 年 8 月 10 日

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

  2. 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] 列表

  3. 路人乙 2020 年 7 月 31 日

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

    • TAT.steph

      TAT.steph 2020 年 8 月 2 日

      已在其他评论回复。

  4. 呀哈哈 2020 年 7 月 28 日

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

  5. 大颂颂 2020 年 7 月 28 日

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

    • TAT.steph

      TAT.steph 2020 年 7 月 28 日

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

  6. LFQ 2020 年 6 月 19 日

    思路真好

  7. 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 上有的你可以搜一下。

  8. kevin198 2020 年 5 月 18 日

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

    • TAT.steph

      TAT.steph 2020 年 5 月 19 日

      用 CSS direction 属性控制。

    • 呀哈哈 2020 年 7 月 28 日

      直接 rotateX(180deg) 就行了吧

  9. Twinkle 2020 年 5 月 6 日

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

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

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

  11. 白晓寒 2020 年 4 月 30 日

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

    • TAT.steph

      TAT.steph 2020 年 4 月 30 日

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

发表评论到 kevin198