React 實現一個漂亮的 Table
概述
對於企業級後臺產品來說,Table 應該是使用最頻繁的元件了,它通常比 Form 和 Chart 的使用還頻繁。對於這麼一個常用的元件,我們決定要把它從 RSuite 中單獨出來開發,並且要具有一定的通用性,適應很多場景。 首先看一下,Table 完成的效果。
- 預覽地址: https://rsuitejs.com/rsuite-table
- Github: https://github.com/rsuite/rsuite-table
最開始促使我們去實現這個 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 列的表格,接下來我們來看一下具體的功能點。
功能介紹
鎖定列
表頭是預設固定的不需要額外的配置,要固定列,需要在固定的列 <Column>
新增 fixed
屬性。
<Column width={100} fixed>
<HeaderCell>ID</HeaderCell>
<Cell dataKey="id" />
</Column>
這個功能是所有功能裡面最麻煩的,特別是表頭和列同時固定的時候,前面我也提到過 Ant Design 的 Table 就存在一個問題,滾動的時候固定列和非固定列未對齊,以下是一個 Ant Design 的 Table 的一個截圖和訪問連結。
訪問地址: https://ant.design/components/table-cn/#components-table-demo-fixed-columns-header
造成這個問題的主要原因是 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 的列是可以調整寬度的,所以我們也希望列可以調整寬度,只需要在 <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>
在 <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
完整示例程式碼
排序
排序是一個基礎的功能,在需要排序的列 <Column>
設定一個 sortable
屬性。 同時在 <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>
完整示例程式碼
分頁
提供了一個 <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 切換顯示條目數的回撥函式。
看一下,效果:
完整示例程式碼
樹形表格
先看一下樹形表格的樣子
渲染成樹形的表格需要設定兩個地方,首先 <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>
完整示例程式碼
可編輯的表格
可編輯的表格,只需要自定義一個 <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 中很容易實現這個功能,如果整個
<Table>
是的通過 CSS 佈局控制也許能實現這個功能,但是在實現的時候,很多地方都是通過 JS 控制高度,比如: 行高、單元格的 left,top 相對位置等等,所以要根據內容來自動行高是比較麻煩的事情,暫時沒想到好的解決辦法,但是我們提供了一個onRerenderRowHeight
函式,可以讓使用者自己根據內容來控制行高。 - 根據內容自動設定列寬, 這個問題暫時也沒有想到好的解決方案, 現在只能通過
flexGrow
來填充剩餘寬度。 - 固定列在右側,這個功能後續會考慮加進去。
- 表頭分組,合併單元格,這個功能麻煩點在於,我們所有的列都是可以調整列寬的,如果同時考慮合併單元格邏輯上處理有些麻煩,不過後續會考慮加入該功能。
如果,你對這些問題有好的想法歡迎你 提交 pull request。 如果,你在使用中存在任何問題,可以提交 issues。