標籤:

React 實現一個漂亮的 Table

概述

對於企業級後台產品來說,Table 應該是使用最頻繁的組件了,它通常比 Form 和 Chart 的使用還頻繁。對於這麼一個常用的組件,我們決定要把它從 RSuite 中單獨出來開發,並且要具有一定的通用性,適應很多場景。 首先看一下,Table 完成的效果。

  • 預覽地址: rsuitejs.com/rsuite-tab
  • Github: github.com/rsuite/rsuit

最開始促使我們去實現這個 Table 組件是因為產品經理希望表格可以像 Excel一樣固定表頭和列,我們都知道 HTML table 是不支持這個功能,但是在實際應用中,對於數據行多列多的情況下,固定表頭和列非常有用,方便數據關聯瀏覽。我們的組件庫都是 React 的, 開源環境中也沒有找到一個適合的我們的 Table 組件。 Ant Design 中的 Table 估計有些人用過,UI 比較漂亮,但是在固定表頭和列的這個功能上我還是有些不滿意,特別是要同時固定表頭和列的時候,在 Retina 屏幕上,Ant Table 通過觸摸板滾動表格,固定的區域和非固定的區域會對不整齊,看上去會抖動,這個體驗不是特別好。我知道 facebook 的 FixedDataTable 針對這塊的處理做的還不錯,是一個好的參考,特別是大數據量渲染也不卡頓,但是有些功能也不能滿足我們的業務場景,比如在要 Table 中呈現一個樹形結構就沒有這個功能。所以還是決定自己造這個輪子。

設計

在 UI 的設計上符合 RSuite 的整體風格。 我們具體看一下組件的設計,整個 Table 提供了 5 個組件,分別是:

  • <Table> 定義表格,可以設置數據源,表格類型等等
  • <Column> 定義列,設置列與數據源關聯的 key, 設置列寬度,設置是否可以排序,是否需要固定列等等。
  • <Cell> 定義單元格,用於渲染數據的組件,可以自定義顯示的方式。
  • <HeaderCell> 定義列頭的單元格。
  • <TablePagination> 定義分頁,是一個可選組件。

看一個簡單的示例:

npm i rsuite rsuite-table --save

有些地方依賴了 RSuite 中的基礎組件,所有需要安裝 rsuite。

import { Table, Column, HeaderCell, Cell } from rsuite-table;<Table data={data} > <Column width={100} sort fixed resizable> <HeaderCell>ID</HeaderCell> <Cell dataKey="id" /> </Column> <Column width={100} sort resizable> <HeaderCell>Name</HeaderCell> <Cell dataKey="name" /> </Column> <Column width={100} sort resizable> <HeaderCell>Email</HeaderCell> <Cell dataKey="email" /> </Column></Table>

這是一個簡單 3 列的表格,接下來我們來看一下具體的功能點。

功能介紹

鎖定列

表頭是默認固定的不需要額外的配置,要固定列,需要在固定的列 &lt;Column> 添加 fixed 屬性。

<Column width={100} fixed> <HeaderCell>ID</HeaderCell> <Cell dataKey="id" /></Column>

這個功能是所有功能裡面最麻煩的,特別是表頭和列同時固定的時候,前面我也提到過 Ant Design 的 Table 就存在一個問題,滾動的時候固定列和非固定列未對齊,以下是一個 Ant Design 的 Table 的一個截圖和訪問鏈接。

訪問地址: ant.design/components/t

造成這個問題的主要原因是 onScroll 觸發的頻率和渲染的速度跟不上造成的, 如果要列和表頭都固定,那必然會在一個方向上需要手動修改元素的位置,這裡肯定不能用 React state 存儲位置,然後等待渲染,那太慢了。所有需要操作 DOM, 去改變元素的位置,這裡有這幾個需要注意的技術點:

  • 用 transform: translate3D 代替 top 與 left ,因為 top/left 會導致迴流,而 translate 只產生重繪,性能會更好,另外 translate3D 走的是 3D, 在手機瀏覽器器上會 GPU 加速。
  • onScroll 觸發的頻率和渲染的速度會存在跟不上的情況,所有這裡最好是自己實現一個滾動條,在 Table Body 上監聽 onWheel 事件,在滾動條上監聽 onMouse* 事件。 在自己實現滾動條的時候需要注意的是,在 Mac 的 chrome 上,左右滑動的時候會觸發瀏覽器的上一頁和下一頁功能,所以這裡的事件冒泡要處理好(本來想找一個開源的滾動條輪子,發現有好多組件這個問題沒有處理好,所以就自己寫了)。

對 DOM 操作用到了 dom-lib

我們的 Table 在處理上面兩點以後,就解決了 Ant Design 的 Table 滾動存在的問題,當然如果大家有更好的方案,感謝你分享一下。 另外,Ant Table 有很多方面做得是比我們好的,比如它支持固定右側的列,支持嵌套表格等等功能。

完整示例代碼

可調整列寬

在表格中有些列的數據有長有短,不太好預測,但還是希望在一個單元格內顯示,如果給列固定好一個寬度以後,那超出單元格的內容就會被截斷隱藏,導致信息顯示不完整。Excel 的列是可以調整寬度的,所以我們也希望列可以調整寬度,只需要在 &lt;Column> 設置一個 resizable 屬性。

<Column width_={130} sortable> <HeaderCell>First Name</HeaderCell> <Cell dataKey="firstName" /></Column>

完整示例代碼

自動設置列寬

有一種情況,Table 在頁面中的寬度比如是 1000px+ (可能更寬,根據顯示器屏幕的寬度決定), 但是這個 Table 只有 3 列,如果每列都固定一個 200px, 肯定 撐不滿整個 Table,導致不美觀, 我們都知道 HTML table, 當給 table 設置 width:100% 以後,列會根據內容自動撐滿,如果給其中一個 td 設置了 width, 那 Table 剩下的 width, 會被剩下的幾列撐滿。那在 rsuite-table 怎麼解決問題呢? 看以下示例:

<Table width_={1000}> <Column width_={100}> <HeaderCell>First Name</HeaderCell> <Cell dataKey="firstName" /> </Column> <Column flexGrow={1}> <HeaderCell>City</HeaderCell> <Cell dataKey="city" /> </Column> <Column flexGrow={2}> <HeaderCell>Company Name</HeaderCell> <Cell dataKey="companyName" /> </Column></Table>

在 &lt;Column> 組件上提供了一個 flexGrow 屬性,有點類似 CSS3 中的 flex-grow 屬性。上面示例中,Table 的 width 為 1000 , 第一列的 width:100, 第二列設置為 flexGrow:1, 第三列設置為 flexGrow:2。 渲染後計算的結果是:

  • 第一列: 100px
  • 第二列: flexGrow:1, (1000 - 100)/(2 + 1) * 1 = 300px
  • 第三列: flexGrow:2, (1000 - 100)/(2 + 1) * 2 = 600px

完整示例代碼

排序

排序是一個基礎的功能,在需要排序的列 &lt;Column> 設置一個 sortable 屬性。 同時在 &lt;Table> 定義一個 onSortColumn:Function 回調函數,點擊列頭排序圖標的時候,會觸發該方法,並返回 sortColumn:String 和 sortType:String(asc|desc)。 看一下示例:

<Table onSortColumn={(sortColumn, sortType)=>{ console.log(sortColumn, sortType); }} > <Column width_={50} sortable> <HeaderCell>Id</HeaderCell> <Cell dataKey="id" /> </Column> <Column width_={130} sortable > <HeaderCell>First Name</HeaderCell> <Cell dataKey="firstName" /> </Column> <!--... --></Table>

完整示例代碼

分頁

提供了一個 &lt;TablePagination> 組件,用於顯示分頁欄,這裡的分頁需要開發人員自己去處理數據,看一下示例代碼:

function formatLengthMenu(lengthMenu) { return ( <div className="table-length"> <span> 每頁 </span> {lengthMenu} <span> 條 </span> </div> );}function formatInfo(total, activePage) { return ( <span>共 <i>{total}</i> 條數據</span> );}<TablePagination formatLengthMenu={formatLengthMenu} formatInfo={formatInfo} displayLength={100} total={500} onChangePage={this.handleChangePage} onChangeLength={this.handleChangeLength}/>

  • formatLengthMenu 格式化顯示行數;
  • formatInfo 格式化顯示總條目信息;
  • displayLength 默認顯示多少行數據,可以通過 state 管理;
  • total 它不是當前返回數據的行數,他是所有數據的總條目數,這個需要後端 API 的返回,通過這個值與displayLength,才能計算出表格分多少頁。可以通過 state 管理;
  • onChangePage 切換分頁的回調函數;
  • onChangeLength 切換顯示條目數的回調函數。

看一下,效果:

完整示例代碼

樹形表格

先看一下樹形表格的樣子

渲染成樹形的表格需要設置兩個地方,首先 &lt;Table> 組件上設置一個 isTree 屬性,同時 data 中的數據需要通過 children 來定義關係結構。

<Table data={data} isTree expand height={400}>

data 中的數據結構

[{ labelName: 汽車, status: ENABLED, children: [ { labelName: 梅賽德斯-賓士, status: ENABLED, count: 460 } ... ] ...}]

完整示例代碼

自定義單元格

單元格中的內容往往需要能交互的,比如設置為一個連接,或者 hover 的時候能顯示一段信息等等。 在 rsuite-table 中,可以對 Cell 進行自定義。 先看一下以下是一個自定義後的表格圖例:

比如,顯示一個圖片,定義一個 ImageCell 組件:

const ImageCell = ({ rowData, dataKey, ...props }) => ( <Cell {...props}> <img src={rowData[dataKey]} width_="50" /> </Cell>);

用的時候:

<Column width_={200} > <HeaderCell>Avartar</HeaderCell> <ImageCell dataKey="avartar" /></Column>

比如,要格式化日期,就定義一個 DateCell 組件:

const DateCell = ({ rowData, dataKey, ...props }) => ( <Cell {...props}> {rowData[dataKey].toLocaleString()} </Cell>);

用的時候:

<Column width_={200} > <HeaderCell>Action</HeaderCell> <DateCell dataKey="date" /></Column>

自定義行高

如果在實際應用中需要根據數據內容來定義行高,可以使用以下方式

<Table onRerenderRowHeight={(rowData) => { if (rowData.firstName === Janis) { return 30; } }} >...</Table>

完整示例代碼

可編輯的表格

可編輯的表格,只需要自定義一個 &lt;Cell>, 然後通過 state 管理狀態。

export const EditCell = ({ rowData, dataKey, onChange, ...props }) => { return ( <Cell {...props}> {rowData.status === EDIT ? ( <input className="input" defaultValue={rowData[dataKey]} onChange={(event) => { onChange && onChange(rowData.id, dataKey, event.target.value); }} /> ) : rowData[dataKey]} </Cell> );};

完整示例代碼

遺留的問題

  • 內容自動換行,並且自動設置行高,在 HTML table 中很容易實現這個功能,如果整個 &lt;Table> 是的通過 CSS 布局控制也許能實現這個功能,但是在實現的時候,很多地方都是通過 JS 控制高度,比如: 行高、單元格的 left,top 相對位置等等,所以要根據內容來自動行高是比較麻煩的事情,暫時沒想到好的解決辦法,但是我們提供了一個 onRerenderRowHeight 函數,可以讓用戶自己根據內容來控制行高。
  • 根據內容自動設置列寬, 這個問題暫時也沒有想到好的解決方案, 現在只能通過 flexGrow 來填充剩餘寬度。
  • 固定列在右側,這個功能後續會考慮加進去。
  • 表頭分組,合併單元格,這個功能麻煩點在於,我們所有的列都是可以調整列寬的,如果同時考慮合併單元格邏輯上處理有些麻煩,不過後續會考慮加入該功能。

如果,你對這些問題有好的想法歡迎你 提交 pull request。

如果,你在使用中存在任何問題,可以提交 issues。


推薦閱讀:

React Conf 2017 不能錯過的大起底——Day 1!
解析 Redux 源碼
我對Flexbox布局模式的理解
從0到1,細說 React 開發環境搭建那點事
Immutable 詳解及 React 中實踐

TAG:React |