1. 程式人生 > >React 實現一個漂亮的 Table

React 實現一個漂亮的 Table

概述

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

rsuite

  • 預覽地址: 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 列的表格,接下來我們來看一下具體的功能點。

功能介紹

鎖定列

表頭是預設固定的不需要額外的配置,要固定列,需要在固定的列 &lt;Column> 新增 fixed 屬性。

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

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

ant-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 的列是可以調整寬度的,所以我們也希望列可以調整寬度,只需要在 &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 切換顯示條目數的回撥函式。

看一下,效果:

pagebar

完整示例程式碼

樹形表格

先看一下樹形表格的樣子

tree

渲染成樹形的表格需要設定兩個地方,首先 &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 進行自定義。 先看一下以下是一個自定義後的表格圖例:

custom-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>

完整示例程式碼

可編輯的表格

edit-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。