一起來聊聊table元件的固定列

網易考拉前端團隊發表於2017-12-25

檢視原文

現有Web頁面上,table一般用於統計資料、列表書記等資訊的展示。但是當table包含大量的資訊需要列出展示時,往往對table新增滾動條來展示更多的資料。突然想到要研究下table元件也是因為最近碰到table資料大量展示的問題,尋覓了一些比較好的解決辦法,就目前來說,個人已知最好的應該就是ant-design中的Table表格。它通過對錶格前後列進行固定,中間滾動的互動方式可以很好的滿足個人需求,這種互動對於大量表格資料和關鍵資訊的展示非常友好,下面我們就來聊聊包含大量資料和列數的表格展示優化。

表格功能

首先需要對錶格的大致功能有一個大概的瞭解。Ant-design上對錶格的使用說明如下:

當有大量結構化的資料需要展現時;當需要對資料進行排序、搜尋、分頁、自定義操作等複雜行為時。
通常來說,表格的主要用途就是資料的展示,然後還附帶一些其他的功能來滿足對資料的整理、篩選等。

表格設計

表格一般包括表頭、表尾,內容對應具體的行列,每一個資料佔據一個單元格,如下圖所示。

表格
根據HTML中表格的標籤屬性,我們對錶格進行拆分:

  • 表頭
  • 表尾
  • 單元格

對應常用的功能項包括:

  • 選擇等操作
  • 篩選和排序
  • 展開資料
  • 表格行列合併
  • 樹形資料
  • 固定表頭、表尾
  • 固定列
  • 固定頭和列
  • 編輯

大量資料的表格互動優化

對於包含大量資料和列數的表格,新增滾動條是一種常見的方法,用來提示表格包含更多的內容。但是,這種方法往往不夠簡潔,並且不容易展示關鍵的資訊。

滾動條往往太過笨重,在視覺上喧賓奪主,因此,現代作業系統已經開始簡化它的外觀,當使用者不與可滾動的元素互動時,滾動條就會被隱藏。(《CSS揭祕》34-滾動條提示)

對於大量資料的表格,更好的使用者體驗優化主要包含以下兩點:

  1. 更優雅的使用者體驗模式——以陰影來提示更多的內容 在css揭祕中有提及到:“Google Reader的使用者體驗設計師找到了一種非常優雅的方式來做出和滾動條提示類似的提示:當側邊欄的容器還有更多的內容時,一層淡淡的陰影會出現在容器的頂部或者底部”。 對於大量資料的表格,我們可以以類似的方式來提示使用者表格包含更多的內容,需要滾動。
  2. 關鍵資訊展示——固定列&表頭 對於表格資訊來說,往往有些資訊是重要的,需要一目瞭然的看清楚,而不是通過滾動才能看到使用者需要的資訊。將關鍵的表格列進行固定,而次要資訊放置在可滾動檢視的容器中,可以突出表格資訊的重要性,同時,對於方便使用者更好的在當前視窗進行互動,而不需要更多的操作。

對於以上兩種互動的優化,Ant Design - A UI Design Language給出了很好的示例。

Ant-design-table

優化的實現

對於固定頭和列的表格資料互動方式,現有的元件庫均有對應的實現,比較常見的對應有:

那麼如何實現上述表格的互動優化呢,綜合以上三種元件庫對於table元件的實現方式來看,具體包括以下幾個方面(以下文中示例程式碼使用vue實現)。

佈局

在考慮表頭和列進行固定時,我們首先需要考慮的是如何對錶格進行佈局來同時滿足上下和左右的滾動。

表格滾動示例
如上圖所示,藍色框框部分將表格分為固定的表頭和內容,內容區域可以上下滾動;紅色框框部分將表格分類固定的列和中間列,中間部分可以左右滾動。從示意圖可以發現,兩部分有一部分內容交叉。 在佈局上,主要實現方式為以下兩種:

  1. 如果依據左右滾動佈局,那麼整個表格分為左,中,右三部分,中間overflow左右滾動。那麼,如果同時滿足表頭固定後,body上下滾動,需要js事件監聽中間部分的上下滾動,同時對左右表格的body部分進行滾動。見表格左右滾動佈局示例
  2. 如果依據上下滾動佈局,那麼,整個表格將分為兩大部分,thead和tbody,內容部分溢位後滾動。那麼,如果需要同時滿足中間部分進行左右滾動,需要在tbody左右滾動時,同時控制thead的中間進行滾動,需要js監聽tbody的scroll事件,對thead進行滾動。見表格上下滾動佈局示例

這兩種方式均需要通過js監聽scroll事件,來滿足另一部分的滾動事件,示例的demo比較簡陋。對比這兩種方式,可以說實現上都差不多,現有元件庫的實現均為第一種方式。採用這一類佈局的共同點和優勢如下:

  • 將表頭和內容分開為兩部分,便於控制表頭固定。
  • 通過佈局滿足上下或者左右的滾動,通過js控制另一方向的滾動,相對於完全通過js控制滾動更有優勢。
  • 需要特別注意的一點是,中間內容的表格需要包含左右固定的兩列,而不是完全拆分,左右固定的列僅僅是通過固定佈局覆蓋現有的資料。這樣做的優勢是保證在中間部分滾動的時候,滾動條屬於整個table表格而不是中間部分,給使用者視覺上的整個表格滾動的互動體驗,這樣也保證了一個方向上的完全滾動。
  • 左右滾動佈局需要在body滾動的同時控制左側固定列和右側固定列同時滾動,相對於上下滾動佈局僅僅需要控制表頭內容左右滾動來說,減少一次計算控制。本以為佈局2在資料量大的情況下,相對佈局一發生抖動的可能性更大,但是在5000行資料測試下(僅測試chrome),發現滾動時,兩者相差不大。

固定行列對齊

由於表格的表頭、列固定方法採用的是從table中拆分的方法,固定的表格部分和其餘部分可能不一致。同時,表格的內容不固定的情況下,其佈局也是比較難以預測的,瀏覽器往往會根據內容對錶格列寬進行調整。

因此,我們在真正使用表格進行資料展示時,由於資料的不可控性,展示出來的效果可能和我們預計的不一樣,如下圖所示。

::表格行不對齊::

表格行不對齊
::表格列不對齊::
表格列不對齊
對於這兩種情況,解決方式如下:

  1. 列不對齊,給表格每一列固定寬度,通過定寬的方法來控制表格。
  • 設定table的佈局方法為table-layout: fixed。 table-layout的預設值是auto,其行為模式被稱作自動錶格佈局演算法。將表格的佈局方式設定為fixed不僅有利於頁面的快速渲染,同時也讓表格更可控。具體的規範見:table-layout - CSS | MDN

使用 “fixed” 佈局方式時,整個表格可以在其首行被下載後就被解析和渲染。這樣對於 “automatic” 自動佈局方式來說可以加速渲染,但是其後的單元格內容並不會自適應當前列寬。任何一個包含溢位內容的單元格可以使用 overflow 屬性控制是否允許內容溢位。

  • 指定表格上每一列的寬度。設定寬度的方法有兩種,其一是對每一個th指定width,另一種是使用col屬性相對來說,col的方式和對每一個th設定寬度方式比,更優雅一點。我們還可以通過來對錶格中的列進行組合,以便對其進行格式化。不過需要注意的一點是,目前的瀏覽器對於col和colgroup,很多屬性已經被廢棄了,目前支援度比較全面的僅span和width這兩個屬性,分別用於規定列組應該橫跨的列數和列的寬度。
  • 設定寬度和table-layout後,如果整個表格的寬度大於所有列設定的寬度,且所有列均設定寬度,那麼會將多餘的寬度按照設定的列寬度比例平均分配。如果只設定了部分列,那麼平均分配到剩下未指定寬度的所有列中;如果整個表格寬度小於所有列的設定寬度,且所有列均設定寬度,那麼表格會被列撐開。如果只設定了部分列,沒有設定寬度的列會被壓縮寬度。
  1. 行不對齊,實現方式有三種。
  • 如果表格中展示的資料不需要完全展示,可以指定表格行的高度,當表格資料溢位時,自動出現省略號來防止表格行左右不對齊的情況。這種方式適用於已知資料的,固定內容的表格資料展示。
  • 對左右固定列的兩個table,不僅僅包含固定列的資料,並且包含所有表格資料。這樣表格行高依據於整個表格一行的資料,並且保持左、中、右資料的完全一致,不會出現左右行高不一致導致行不對齊。這種方式會導致html中擁有較多的冗餘資料,不太利於語義化。
  • 繪製時,計算一次中間body的表格行高度,根據中間行高來設定左右固定列的表格行高(Ant-design的實現方法)。這種方式減少了冗餘資料,但是在初始js渲染時,可能需要更長的時間。

左右陰影提示

給table左右固定列均增加一個淡淡的陰影,可以很好的給使用者以“這裡還有更多資料”的提示,同時,陰影也讓固定列的資料更有層次感。 例如通過給左右固定列增加box-shadow,可以增加陰影效果,實現css如下:

.table-fixed-left-scroll {
  box-shadow: 6px 0 6px -4px rgba(0,0,0,.2);
}
.table-fixed-right-scroll {
  box-shadow: -6px 0 6px -4px rgba(0,0,0,.2);
}
複製程式碼

就現有的大部分元件庫來說,僅僅做了陰影的實現,當我們滾動時,這條陰影會一直停留在相同的位置。相對來說,更好的互動效果是和Ant-design的table一樣,陰影會隨著我們的滾動而自動出現,這樣給使用者的提示也就更加友好,同時可以增加transition動畫來讓左右陰影出現的更為自然。

自動陰影提示
實現方式 監聽body區域scroll事件,通過判斷scrollLeft以及整個整個body區域是否被overflow的寬度,來判斷左陰影和右陰影。同時需要注意的是:在視窗resize的時候,需要對左右陰影進行重新計算。為了減少scroll事件時整個區域被overflow的寬度的計算(計算clientWidth會增加一次瀏覽器的重排),在整個視窗onload和resize成功後,需要對視窗進行一次計算,並將計算結果快取。

/* onscroll */
handleBodyScroll() {
      this.scrollValue = this.$refs.tableScroll.scrollLeft;
      this.hasRight = this.scrollValue - this.leftScroll < 0;
      this.hasLeft = this.scrollValue > 0;
},
/* onload & onresize */
setTableShadow() {
      this.leftScroll = this.$refs.tableContent.clientWidth - this.$refs.tableScroll.clientWidth;
      this.handleBodyScroll();
}
複製程式碼

更進一步的優化嘗試

對於需要固定表頭和表列的table,往往資料量都比較大。我們可以對現有的實現方式做更進一步的優化嘗試。

使用tranfrom來代替設定scrollLeft

考慮瀏覽器重排和重繪,一般來說,scrollLeft會引起一次重排,而transfrom僅僅是一次重繪,同時我們可以使用transform3D使用GPU來加速瀏覽器的渲染。那麼,是不是可以考慮在scroll事件中,使用transform來代替scrollLeft的設定呢? 在React 實現一個漂亮的 Table | HYPERS 前端團隊部落格這篇文章中,有提及到實現一個優化的table元件使用下面兩點來優化onScroll 觸發的頻率和渲染的速度跟不上造成的抖動問題,他的解決辦法如下:

用 transform: translate3D 代替 top 與 left ,因為 top/left 會導致迴流,而 translate 只產生重繪,效能會更好,另外 translate3D 走的是 3D, 在手機瀏覽器器上會 GPU 加速。

onScroll 觸發的頻率和渲染的速度會存在跟不上的情況,所有這裡最好是自己實現一個滾動條,在 Table Body 上監聽 onWheel 事件,在滾動條上監聽 onMouse* 事件。 在自己實現滾動條的時候需要注意的是,在 Mac 的 chrome 上,左右滑動的時候會觸發瀏覽器的上一頁和下一頁功能,所以這裡的事件冒泡要處理好(本來想找一個開源的滾動條輪子,發現有好多元件這個問題沒有處理好,所以就自己寫了)。

為了驗證transform和scroll實際效率的對比,使用5000行資料對之前的demo進行了scroll抖動的測試,發現兩種佈局方式對應的效率提升並沒有很明顯。

::左右佈局-scroll::

左右佈局-scroll

::左右佈局-transform3D::

左右佈局-transform3D

::上下佈局-scroll::

上下佈局-scrol

::上下佈局-transform3D::

上下佈局-transform3D

從以上四個圖可以對比看出,tranfrom3D和scroll的效能差距並沒有很大。我們還可以通過jsperf 對兩者進行兩者的效能分析,同樣得到transfrom3D和scroll對滾動效能的影響並沒有相差很大這個結論。

減少scroll事件中的dom操作

本文中的Demo只是簡單的示例,為了更好的對比table的scroll效能,通過對iview和element兩個元件庫的table均進行了5000行資料下table滾動的測試,發現兩者均有一定的抖動(都有做防抖動處理,不夠明顯)。 ::iview-table-scroll抖動::

iview-table-scroll抖動

::element-table-scroll抖動::

element-table-scroll抖動

檢視兩個元件庫的原始碼,發現兩者均在scroll事件中執行了較多的邏輯判斷,例如iview在scroll中進行了column是否存在的判斷,時,每次渲染都對操作那一列進行了重新渲染計算,element在scroll時,對左右和上下的滾動都進行了判斷計算,這些也都影響滾動的效能。由於onscroll的高頻觸發,儘量減少scroll事件中對dom的計算和操作,減少瀏覽器重排和重繪。

優化滾動事件

對於onscroll,onresize等這一類高頻觸發的事件,如果事件中涉及到大量的位置計算、DOM 操作、元素重繪等工作且這些工作無法在下一個 scroll 事件觸發前完成,就會造成瀏覽器掉幀。加之使用者滑鼠滾動往往是連續的,就會持續觸發 scroll 事件導致掉幀擴大、瀏覽器 CPU 使用率增加、使用者體驗受到影響。

對使用者來說,平滑的滾動往往能帶來很好的互動效果,優化滾動事件往往有以下三種方法:

  • 防抖(Debouncing):把多個操作合併為一個,即在一定時間內,規定事件出發的次數。
  • 節流(Throttling):只允許操作在一定時間內執行一次,只有當上一次操作執行後過了你規定的時間間隔,才能進行下一次。這種方法往往運用於圖片懶載入的的優化。
  • 使用 rAF(requestAnimationFrame)觸發滾動事件:在頁面重繪之前,通知瀏覽器進行操作。 一般來說,現有的table元件中,一般採用第一種方式來對scroll事件進行處理,通過約束一定時間內發生的次數,來因為使用者過快操作導致的scroll請求。

使用純CSS實現陰影自動提示

在《CSS揭祕》一書的第34小節滾動提示中(具體文章可見:Pure CSS scrolling shadows with background-attachment: local | Lea Verou),對這種方法進行詳細的闡述,其原理實現是通過設定兩層背景來得到陰影,或者通過設定偽元素和定位來實現,方法實現如下:

  • 兩層背景:play.csssecrets.io/scrolling-hints
  • 偽元素和定位:Scrolling shadows 對於table元件,實現css如下:
.tablebackground {
  background: linear-gradient(white 15px, hsla(0,0%,100%,1)) 100px 0 / 15px 300px,
              radial-gradient(at left, rgba(0,0,0,.2), transparent 80%) 100px 0 / 10px 200px,
              linear-gradient(to left, white 15px, hsla(0,0%,100%,1)) right / 110px 300px,
              radial-gradient(at right, rgba(0,0,0,.2), transparent 70%) 390px / 15px 300px;
  background-repeat: no-repeat;
  background-attachment: local, scroll, local, scroll;
}
複製程式碼

實現原理 通過background-attachment這個屬性,它包含fixed、local和scroll三種取值。

fixed:此關鍵字表示背景相對於視口固定。即使一個元素擁有滾動機制,背景也不會隨著元素的內容滾動。

local:此關鍵字表示背景相對於元素的內容固定。如果一個元素擁有滾動機制,背景將會隨著元素的內容滾動, 並且背景的繪製區域和定位區域是相對於可滾動的區域而不是包含他們的邊框。

scroll:此關鍵字表示背景相對於元素本身固定, 而不是隨著它的內容滾動(對元素邊框是有效的)。

具體實現為:通過構建兩層背景,一層用於生成陰影,使用scroll屬性,預設和內容保持在原位;另一層為一個用來遮擋陰影的白色矩形,使用local屬性,相對於可滾動區域位置固定,這樣它就會在滾動在最左側或者右側時蓋住陰影,滾動時跟著滾動,露出陰影。 同時,使用線性漸變的遮罩層可以讓滾動時陰影出現的更為平滑,具體的實現效果請看demo純css實現table陰影自動提示

這種純CSS的實現很好的避免了通過js監聽事件來對dom節點進行操作,更好的優化了效能,但是這種寫法依然存在一定的問題:

  1. 我們在對table設定遮罩層時,是從左邊固定的位置開始的,如果中間的table內容為整個table的寬度,那麼需要指定陰影開始的位置,即需要知道左邊固定列和右邊固定列的寬度,比較適用於固定列寬度已知的table。
  2. 可以考慮將中間的視窗大小僅僅設定為非固定列的table部分,這樣不需要計算固定列的寬度,但是這樣出來的瀏覽器自帶滾動條不能涵蓋整個table。

總結

對於table表頭和列固定,在實現時需要注意以下幾點:

  1. 存在表頭和固定列時,需要對table的佈局進行重組,一般固定一個方向進行overflow,另一方向通過監聽onscroll事件來同步滾動。
  2. 需要固定表頭和列時,特別注意table的每一列的寬度和每一行的高度,可以使用一些方法來減少因為寬窄螢幕和資料的不確定性導致的錯位。
  3. 增加左右固定列的自動陰影提示可以給使用者更好的互動體驗。實現方式為通過監聽scroll事件,使用js來實現,或者通過純CSS設定背景圖來實現。相對來說純CSS實現減少了dom操作,不過需要結合table的佈局方式注意下背景陰影的位置。
  4. 儘量減少scroll事件內對dom節點的操作,簡化 scroll 內的操作。
  5. 採用一定的防抖動或者節流等方法來更好的平滑滾動效果,提高頁面效能。
  6. 固定表頭和列一般用於大資料的table展示,在寫常用table元件時,需要注意table的通用的使用場景,不能因為考慮一些極端情況而因小失大,更多的考慮相容表頭和列的固定即可。

參考資料

by 鄧瑾

相關文章