緣起
標題有點誇張,並不是完全放棄antd-table,畢竟在react的生態圈里,對國人來說,比較好用的PC端組件庫,也就antd了。即便經歷了2018年聖誕彩蛋事件,antd的使用者也不僅不減,反而有所上升。
客觀地說,antd是開源的,UI設計得比較美觀(甩出其他組件庫一條街),而且是螞蟻金服的體驗技術部(一堆p7,p8,p9,基本都是大牛級的)在持續地開發維護,質量可以信任。
不過,antd雖好,但一些組件在某一些場景下,是很不適用的。例如,以表格形式無限滾動地展示大量數據(1w+)時,antd-table就特別蹩腳了,光是首次渲染就能卡個五秒白屏。如果這個表格還要求能編輯,甚至不同列之間發生聯動呢?對不起,antd-table無能為力,會把頁面卡炸的。
antd-table本身是基於rc-table的擴展,而rc-table所屬的react-component素來有自己的主張,在react社區其他的組件庫都支持無限滾動時(例如react-data-grid, react-virtualized, react-tabulator..),很抱歉,它不支持。
爹爹不支持,作為兒女的antd-table也不好反對,順其自然咯。
於是,部分使用antd的開發者就腦闊疼了,想使用其他支持無限滾動的表格組件吧,會發現諸多的問題:
1.UI太丑,真的,特別是react-data-grid,不能再丑了。雖然它的功能很強大,但顏值是個硬傷。想給它整容,符合antd一慣的審美風格,還真的挺繁雜的,從上手到放棄系列。
2.擴展起來,不接地氣。有的組件庫,功能很強,但封裝得太厲害,說的就是上面的react-data-grid,還有react-tabulator,要想用起來,可不容易。說是react組件,可怎么用都覺得是反react,有點jq的傾向,惹不起。
3.文檔的可讀性差。react-data-grid,react-virtualized好歹還有基礎的API文檔,雖然寫的不咋地,但也比react-tabulator這個只能讓人去看源碼的強。
4.版本不穩定。react-tabulator很任性,release直接從2,x升級到4.x...
5.不支持樹形表格編輯。說的是react-virtualized,或許新版本支持了,但不得不對它說抱歉。
6.圈子不活躍,人少。人少、不活躍就意味着這個庫可能不長久,比如react-tabulator。
一番比較下來,你會發現,還是react-component舒服,文檔友好,擴展靈活,版本穩定,社區活躍,完全可以嵌套和插入自己寫的react組件(就是丑了點),想必這也是antd基於它來做擴展的一個重要考量。antd或許是意識到了無限滾動地重要性,比如移動端的瀑布流,PC端商品列表的無限下拉刷新,在3.x版本已經基於react-data-grid做了一層擴展,增加了List組件,用來支持無限滾動。
但,對於表格而言,還是沒有人性化的解決方案。
沒辦法,需求來了,不上也得上,自己手寫一個吧。
目前為止,無限滾動沒去做,只做了縱向虛擬滾動,滾動有些許延遲,但首次渲染和編輯的實時響應,還是可以接受的,而且支持固定左右列,橫向滾動,完全支持自定義react組件的嵌套和插入,擴展起來太容易了。基本支持antd-table的用法。
實戰
在動手寫之前,要考慮一些問題:
1.是采用原生table,還是用div來模擬?
2.對於樹形表格,采取怎樣的虛擬滾動方案?
3.組件的職責邊界怎么界定?
一、原生table Vs div模擬表格
table之所以叫table,用意很明顯了,在你想要以表格形式展示數據的時候,首先要想到的,就是用table。
table布局有瀏覽器的特定算法實現加速繪制,且對靜態表格來說,頁面結構是很穩定的。
雖然div模擬表格繪制的速度也不慢,但要達到跟靜態表格一樣的結構穩定性,可就做許多額外的維護工作了,css輔助,js控制,瀏覽器背后對table做的臟活累活,你基本都得接手,從零開始。
但table也有硬傷,首先是樣式不好自定義,想改裝原生table,讓它變得好看,還真不是一件快活的事,具體參考antd-table。其次,如果要求表格左右列能固定,中間列可滾動,原生table就很絕望了,它不得不多叫來兩個table兄弟,讓他們來輔佐自己,一個在左,一個在右,跟自己裝載同樣多的數據,但卻只顯示固定列。三兄弟之間,還要時不時保持聯絡,確保大家每行高度都是一樣的。
如果這中間出了什么偏差,就會導致滾動的表格看起來左邊或右邊的行像是掉了下來....用過antd-table的人,應該會有這樣的體會。
而div模擬表格就不一樣了,它是從零開始的,一張白紙,想怎么畫就怎么畫,要多美就能多美。
要實現左右固定列滾動也不必裝載三份一模一樣的數據,一份就夠了,它要做的,僅僅是把列固定,將固定列鄰居的位置計算好,就能達到同樣的效果。
這里,想看示例,可以看看阿里這位大爺寫的div模擬表格。
基於這個角度的比較,我得給div模擬表格投一票。
二、虛擬滾動方案
首先,得先理解虛擬滾動的概念。
滾動,相信大家都了解,無非就是塊級盒子的內容長度或寬度超出了盒子的寬高,盒子若設置了溢出內容可滾動,那我們就會看到滾動條,可滾動的距離,跟溢出內容所占的長度或寬度是相等的。
<div style="height:30px;overflow:scroll"> <p style="height: 10px">1</p> <p style="height: 10px">2</p> <p style="height: 10px">3</p> <p style="height: 10px">4</p> <p style="height: 10px">5</p> <p style="height: 10px">6</p> </div>
如上述例子,4、5、6是溢出的。它們的高度是30px,即可滾動的距離。
可以預見,如果還有7、8、9…9999等等近一萬條數據,那么這個div同一時刻,最多只能展示4條數據,剩下的9997條數據,都需要滾動才能看到。
創建一個dom節點,成本完全能接受,十個百個千個也可以接受,但上萬數十萬呢?就算能接受,也不該如此浪費。
既然只能在同一時刻看到4個節點,為什么不能只創建4個節點,剩下的節點都是通過滾動要展現的時候,才去創建呢?
這自然是可以的。
虛擬滾動,就是出於這個目的來設計的。
假設數據有6條,這里只討論高度。
如果只創建4個節點,馬上就會發現,滾動條能滾動的距離不對,只有10px。與預期的30px不符。這是因為,滾動距離是瀏覽器根據盒子和盒子里的節點的高度計算出來的。我們只能調整節點的高度,無法直接修改滾動距離的值。
我們可以通過在后面創建一個輔助節點,將高度設為20px來解決這個問題。
<div style="height:30px;overflow:scroll"> <p style="height: 10px">1</p> <p style="height: 10px">2</p> <p style="height: 10px">3</p> <p style="height: 10px">4</p> <p style="height: 20px">占位符</p> </div>
現在,通過監聽div的滾動事件,我們可以知道滾動條滾到了哪個位置,通過計算,得知展示的第一條數據在所有數據中,處於哪個位置,是第2條,還是第1條等等信息...
然后,進一步得知,哪一個未創建的節點,要立即被創建,並且,占位符的高度要對應變化。
例如上述例子里,展示2345的時候,占位符高度就要設為10px,並且最上面也要設置一個10px高的占位符,如:
<div style="height:30px;overflow:scroll"> <p style="height: 10px">占位符</p> <p style="height: 10px">2</p> <p style="height: 10px">3</p> <p style="height: 10px">4</p> <p style="height: 10px">5</p> <p style="height: 10px">占位符</p> </div>
遵循的原則就是,確保2345節點(我們稱之為視圖區)的高度,與占位符的高度加起來,等於總數據的實際總高度。
因此引申出的一個問題就是,每個節點的高度得固定(在表格里,就是固定表格行高)。或者,至少是在徹底展示完成之前,計算出實際高度。前面討論過的組件庫,除了react-data-grid,沒有哪個不是固定行高的。
並且,視圖區的高度也要指定。
如此一來,有了這些不變高度的數值,就能通過監聽滾動來計算上下占位符各自的高度。
虛擬滾動的效果,也就達成了。剩下都是優化的工作,例如緩存節點,diff計算每次滾動時要改變的節點等等。
到這里,我們已經得出了扁平數據列表的虛擬滾動方案。
那么樹形表格呢?
樹形表格,准確的說,指的是數據在表格中以樹形的形式來展現。這樣的表格,可以展開/收起父節點,並且可以嵌套無限層級。參考antd-table的例子。
讓樹形表格支持虛擬滾動,可以利用剛才討論的虛擬滾動方案。
這里的關鍵點在於,樹形數據,是有父子層級關系的,並不是扁平數據。
因而首先要做的,就是把樹形數據按順序遍歷平鋪展開,即扁平化。
// 樹形數據 const tree = [{ node: 1, children: [{ node: 11, children: [] }, { node: 12, children: [] }] }, { node: 2, children: [] }, { node: 3, children: [] }] // 樹形數據按順序平鋪展開 const flatten = [{ node: 1 }, { node: 11 }, { node: 12 }], { node: 2 }], { node: 3 }]]
如此一來,我們就可以完全復用討論過的虛擬滾動方案,達成樹形表格虛擬滾動的效果。
其次,樹形表格的展現,一般是要根據層級的深度來縮進的,這樣才美觀。我們可以展開樹形數據的時候,將層級深度記錄下來,在創建節點的時候,根據層級深度來決定縮進的寬度。
這里,會遇到一些樣式上的問題,比如展開圖標、縮進的寬度,有可能會受到css規則的影響,使得實際效果與預期不符,這個就需要自己去排查解決了。
三、組件的職責邊界
上面已經提到如何實現一個虛擬滾動的樹形表格,但沒提到樹形表格怎么展開、收起子元素,更沒提到表格的可編輯功能。
這涉及到組件職責邊界的確定,也是現在要討論的。
一個組件,特別是react組件,它應該有什么樣的功能,能提供什么樣的API以供擴展,是要考慮清楚的。考慮不清楚的,就像react-tabulator,寫個自定義單元格編輯器都得尋找dom節點,跟JQ有什么區別,而且還要按照它們定的規則來寫,否則就不起作用。
理想的組件,不應該附加額外的規則,而是利用現有的規則,加以合適的運行機制,來達到方便擴展的目的。
antd-table這點做的還算可以,我們只需要將自己的react組件跟提供的API對接,就能達成想要的效果。
所以,我們來確定一下虛擬滾動的樹形表格,應該有怎樣的職責邊界。
首先,列出這表格該有的基礎功能:
1.支持虛擬滾動
2.支持單元格自定義--任何dom節點或者react組件
3.支持左右列固定
沒錯,跟antd-table相比,只是多出了一個虛擬滾動。除此以外的其他功能,都應該是由表格的使用者來實現,諸如可編輯單元格,樹形表格如何展開收起。
這些,可用一句話來總結——數據驅動視圖。
如果用過D3,相信非常能理解這個理念。數據千變萬化,組件的功能也能千變萬化,這是很理想的狀態。
這三個基礎功能里,第1個可以采用上述的虛擬滾動方案來實現。第3個可以用css的sticky屬性配合js計算來實現(具體不贅述,參考阿里大爺的例子)。
第2個,其實倒是最簡單的了。
只需要用React編寫每個單元格容器,就能做到支持單元格的自定義。因為react天生支持dom節點的嵌套,更是本身就支持react組件之間的互相組合。
到此,基於React手寫一個虛擬滾動的表格,已經Over。
行動力強的讀者,應該已經可以寫出自己的demo了。
我寫的表格例子,內部大概長這樣:
<Table onScroll={this.onScroll} style={{ maxHeight: this.tableHeight }}> <TableHead data={data} columns={dataColumns} rowWidth={this.rowWidth} rowKey={this.rowKey} onExpand={this.props.onExpand} /> <Placeholder line={viewUpData.length} height={this.cellHeight * viewUpData.length + 'px'} /> <ViewPort data={data} columns={dataColumns} rowWidth={this.rowWidth} rowKey={this.rowKey} onExpand={this.props.onExpand} /> <Placeholder line={viewDownData.length} height={this.cellHeight * viewDownData.length + 'px'} /> </Table>
外部使用虛擬滾動表格,大概是這樣:
<VirtualTable bordered expandedRowKeys={expandedKeys} rowKey="id" onExpand={(expanded, record) => { this.onExpand(expanded, record) }} dataSource={dataSource} pagination={false} scroll={{ y: 250 }} columns={columns} viewLine={7} onBeforeScroll={this.onBeforeScroll} />
如果之前使用了antd-table來實現功能,那么,只需要將antd-table換成虛擬滾動表格,再加個視圖區的限定於滾動監聽,就完全OK了,不用改變任何原有的業務邏輯。
后續
數據驅動視圖理念的瓶頸,限於我的有限知識,認為應是在於海量數據頻繁快速變化的時候,渲染視圖的速度如何能跟上來,怎樣做到讓人覺得畫面流暢,完全不卡。
比如100萬條數據的下拉滾動。
學海無涯,苦作舟。這條路,一直是會有苦的...