在日常工作中,如下圖的聊天場景是不是很熟悉,沒錯就是我們再熟悉不過的 QQ 和微信,一個正常的聊天界面大致上是長這個樣子的:
這種聊天窗口的消息流有兩個明顯的特點:
- 最新的消息和滾動條初始位置需要在列表的最底部
- 下拉加載歷史消息要在當前消息列表頂部進行銜接
一般來說要實現這樣的功能,對于前端開發來說都不是難事,只要兩步就可以了:
- 在第一屏消息渲染完之后設置容器的 scrollTop 為一個極大值,這樣就把最新消息和滾動條初始位置定位到了最底部;
- 當滾動到頂部時渲染第二屏數據,接著設置容器的 scrollTop 為銜接的位置(也就是第二屏的總高度),這樣就實現了前后兩屏消息的銜接。
這樣的 demo 只需要隨手擼二三十行代碼就實現了:
一開始渲染消息 1~20,滾到頂部后渲染第二屏消息 ABCDEFGHIJK,看上去前后兩屏消息的銜接很平滑很流暢。目前開源社區中也有很多現成的用 React 和 Vue 開發的聊天組件或者示例,他們基本也是用上面提到的思路或者借助 iScroll 實現的。
用上面這種思路跑在 Web 中是沒有任何問題的,但是在小程序中的表現卻大失所望,看一下用同樣的方式應用到小程序后的實際效果:
從第一段視頻(左)可以看到從列表進入到聊天頁面后設置滾動條位置到底部發生了明顯的跳動,先看到停留在頂部然后瞬間再去到底部;
第二段視頻(右)滾動到頂部加載后,下一屏消息與當前消息的銜接出現了一個明顯的跳動,也是先看到在頂部然后才去到預期的位置。
為什么這個思路在 Web 端體驗這么好,到了小程序上體驗就如此糟糕呢?原因其實很簡單,這是由于小程序底層通信邏輯和視圖更新機制造成的:
由于小程序跨線程通信和異步更新的特點,內容的渲染和滾動位置的設置無法保證完成的先后順序,所以必然會先看到上一個位置一閃而過的畫面。
既然是底層的問題,那么這種聊天場景在小程序中難道就玩不了了嗎?當然也有嘗試過用 opacity 過渡和滾動動畫去緩解這種跳動,但都無法從根本上解決這兩個體驗問題。
當各種常規方案嘗試都不盡滿意的時候,那就換個思路:從本質上來說,聊天窗口的消息流實際上是一個 “反自然” 的列表,因為在計算機的 “自然界” 和人們習以為常的使用方式上,列表的初始位置都是在最頂部,想要瀏覽列表更多的內容需要向下滾動,而聊天場景的特點是完全反常規的!
再回到這兩個體驗問題:為什么需要手動設置最新消息和滾動條到最底部,為什么不讓它一開始就在底部?為什么需要要在列表頂部追加數據,為什么不讓它在底部追加數據?所以有沒有可能顛倒常規,做一個 “反向渲染” 的滾動列表呢?答案是肯定的!
首先像常規的列表一樣去渲染,不需要做任何處理,第一條最新消息和滾動條的初始位置是自然地在最上面:
然后把整個列表區域的包裹容器用 css 旋轉 180°,這樣第一條最新消息和滾動條初始位置就在最下面下了:
不過此時整個列表是倒置渲染的,最后再把每一條消息組件用同樣的方式旋轉 180° 使它們顯示回正常的視角,這樣就實現了一個 “反向渲染” 的列表:
雖然是 “反向渲染”,但視覺上和正常的一模一樣。此時頂部就變成了底部,向上追加數據變成了向下追加數據。最后看一下聊天列表使用 “反向渲染” 之后的體驗效果:
可以看到,下拉加載消息與當前消息的銜接非常平滑沒有任何的跳動,實際上這個時候歷史消息是在底部渲染的,只不過反向渲染讓它看上去是在頂部渲染的;此外,頁面一進來最新消息和滾動條位置無需任何處理自然地停留在最底部,接近原生體驗。
這種 “反向渲染” 的思路用最少的代碼就解決了消息場景在小程序上這種幾乎無解的問題,并且達到了最優的體驗,而實際上核心代碼只有兩行 CSS:
transform: rotate(180deg);
direction: rtl;
整個過程無需任何手動設置滾動位置和計算第二屏總高度(實際上都不用關心它們),同樣這種思路用在 Web 上也是 OK 的。當然用了反向渲染也有一些犧牲,比如 IOS 雙擊頂部欄回到頂部這個特點就不能用了,但總體來說獲得體驗上的優化是更多的。
此外,聊天場景中的消息流通常也有這樣的布局:
如果視覺上需要將自己和別人的消息方向分別位列兩邊對齊,那么利用這種 “反向渲染” 的思路,實現起來也非常容易,只需要對消息組件應用不同的 CSS 樣式即可:
消息流的每一條消息都是一個單獨的組件,此時不需要為了區分不同的視角而去新寫一個組件,也不需要改變現有組件的結構布局。
?? 最后
如果你覺得這篇文章對你有幫助,點個「關注/轉發」,讓更多的人也能看到你的分享!