React 實現 Table 的思考

Table 是最經常使用展現數據的方式之一,但是一個產品中每每不少很是相似的 Table,可是咱們碰到的狀況每每是 Table A 要排序,Table B 不須要排序,等等這種看起來很是相似,可是又不徹底相同的表格。這種狀況下,到底要不要抽取一個公共的 Table 組件呢?對於這個問題,咱們團隊也糾結了好久,前後開發了多個版本的 Table 組件,在最近的一個項目中,產出了第三版 Table 組件,可以較好的解決靈活性和公共邏輯抽取的問題。本文將會詳細的講述這種 Table 組件解決方案產出的過程和一些思考。html

Table 的常見實現

首先咱們看到的是不使用任何組件實現一個業務表格的代碼:前端

import React, { Component } from 'react';

const columnOpts = [
  { key: 'a', name: 'col-a' },
  { key: 'b', name: 'col-b' },
];

function SomeTable(props) {
  const { data } = props;

  return (
    <div className="some-table">
      <ul className="table-header">
        {
          columnOpts.map((opt, colIndex) => (
            <li key={`col-${colIndex}`}>{opt.name}</li>
          ))
        }
      </ul>
      <ul className="table-body">
        {
          data.map((entry, rowIndex) => (
            <li key={`row-${rowIndex}`}>
              {
                columnOpts.map((opt, colIndex) => (
                  <span key={`col-${colIndex}`}>{entry[opt.key]}</span>
                ))
              }
            </li>
          ))
        }
      </ul>
    </div>
  );
}

這種實現方法帶來的問題是:react

  • 每次寫表格須要寫不少佈局類的樣式git

  • 重複代碼不少,並且項目成員之間很難達到統一,A 可能喜歡用表格來佈局,B 可能喜歡用 ul 來佈局github

  • 類似可是不徹底相同的表格很難複用數組

抽象過程

組件是對數據和方法的一種封裝,在封裝以前,咱們總結了一下表格型的展現的特色:前端工程師

  • 輸入數據源較統一,通常爲對象數組echarts

  • thead 中的單元格大部分只是展現一些名稱,也有一些個性化的內容,如帶有排序 icon 的單元格函數

  • tbody 中的部分單元格只是簡單的讀取一些值,不少單元格的都有本身的邏輯,可是在一個產品中一般不少相似的單元格佈局

  • 列是有順序的,更適合以列爲單位來添加布局樣式

基於以上特色,咱們但願 Table 組件可以知足如下條件:

  • 接收一個 對象數組全部列的配置 爲參數,自動建立基礎的表格內容

  • thead 和 tbody 中的單元格都可以定製化,以知足不一樣的需求

至此,咱們首先想到 Table 組件應該長成這樣的:

const columnOpts =  [
  { key: 'a', name: 'col-a', onRenderTd: () => {} },
  { key: 'b', name: 'col-b', onRenderTh: () => {}, onRenderTd: () => {} },
];

<Table data={data} columnOpts={columnOpts} />

其中 onRenderTdonRenderTh 分別是渲染 td 和 th 時的回調函數。

到這裏咱們發現對於稍微複雜一點的 table,columnOpts 將會是一個很是大的配置數組,咱們有沒有辦法不使用數組來維護這些配置呢?這裏咱們想到的一個辦法是建立一個 Column 的組件,讓你們能夠這麼來寫這個 table:

<Table data={data}>
  <Column dataKey="a" name="col-a" td={onRenderTd} />
  <Column dataKey="b" name="col-b" td={onRenderTd} th={onRenderTh} />
</Table>

這樣你們就能夠像寫HTML同樣把一個簡單的表格給搭建出來了。

優化

有了 Table 的雛形,再聯繫下寫表格的常見需求,咱們給 Column 添加了 widthalign 屬性。加這兩個屬性的緣由很容易想到,由於咱們在寫表格相關業務時,樣式裏面寫的最多的就是單元格的寬度和對齊方式。咱們來看一下 Column 的實現:

import React, { PropTypes, Component } from 'react';

const propTypes = {
  name: PropTypes.string,
  dataKey: PropTypes.string.isRequired,
  align: PropTypes.oneOf(['left', 'center', 'right']),
  width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  th: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
  td: PropTypes.oneOfType([
    PropTypes.element, PropTypes.func, PropTypes.oneOf([
      'int', 'float', 'percent', 'changeRate'
    ])
  ]),
};

const defaultProps = {
  align: 'left',
};

function Column() {
  return null;
}

Column.propTypes = propTypes;
Column.defaultProps = defaultProps;

export default Column;

代碼中能夠發現 th 能夠接收兩種格式,一種是 function,一種是 ReactElement。這裏提供 ReactElement 類型的 th 主要讓你們可以設置一些額外的 props,後面咱們會給出一個例子。

td 的類型就更復雜了,不只可以接收 functionReactElement 這兩種類型,還有 int, float, percent, changeRate 這三種類型是最經常使用的數據類型,這樣方便咱們能夠在 Table 裏面根據類型對數據作格式化,省去了項目成員中不少重複的代碼。

下面咱們看一下 Table 的實現:

const getDisplayName = (el) => {
  return el && el.type && (el.type.displayName || el.type.name);
};

const renderChangeRate = (changeRate) => { ... };

const renderThs = (columns) => {
  return columns.map((col, index) => {
    const { name, dataKey, th } = col.props;
    const props = { name, dataKey, colIndex: index };
    let content;
    let className;

    if (React.isValidElement(th)) {
      content = React.cloneElement(th, props);
      className = getDisplayName(th);
    } else if (_.isFunction(th)) {
      content = th(props);
    } else {
      content = name || '';
    }

    return (
      <th
        key={`th-${index}`}
        style={getStyle(col.props)}
        className={`table-th col-${index} col-${dataKey} ${className || ''}`}
      >
        {content}
      </th>
    );
  });
};

const renderTds = (data, entry, columns, rowIndex) => {
  return columns.map((col, index) => {
    const { dataKey, td } = col.props;
    const value = getValueOfTd(entry, dataKey);
    const props = { data, rowData: entry, tdValue: value, dataKey, rowIndex, colIndex: index };

    let content;
    let className;
    if (React.isValidElement(td)) {
      content = React.cloneElement(td, props);
      className = getDisplayName(td);
    } else if (td === 'changeRate') {
      content = renderChangeRate(value || '');
    } else if (_.isFunction(td)) {
      content = td(props);
    } else {
      content = formatIndex(parseValueOfTd(value), dataKey, td);
    }

    return (
      <td
        key={`td-${index}`}
        style={getStyle(col.props)}
        className={`table-td col-${index} col-${dataKey} ${className || ''}`}
      >
        {content}
      </td>
    );
  });
};

const renderRows = (data, columns) => {
  if (!data || !data.length) {return null;}

  return data.map((entry, index) => {
    return (
      <tr className="table-tbody-tr" key={`tr-${index}`}>
        {renderTds(data, entry, columns, index)}
      </tr>
    );
  });
};

function Table(props) {
  const { children, data, className } = props;
  const columns = findChildrenByType(children, Column);

  return (
    <div className={`table-container ${className || ''}`}>
      <table className="base-table">
        {hasNames(columns) && (
          <thead>
            <tr className="table-thead-tr">
              {renderThs(columns)}
            </tr>
          </thead>
        )}
        <tbody>{renderRows(data, columns)}</tbody>
      </table>
    </div>
  );
}

代碼說明了一切,就再也不詳細說了。固然,在業務組件裏,還能夠加上公共的錯誤處理邏輯。

單元格示例

前面提到咱們的 tdth 還能夠接收 ReactElement 格式的 props,你們可能還有會有點疑惑,下面咱們看一個 SortableTh 的例子:

class SortableTh extends Component {
 static displayName = 'SortableTh';

 static propTypes = {
    ...,
    initialOrder: PropTypes.oneOf(['asc', 'desc']),
    order: PropTypes.oneOf(['asc', 'desc', 'none']).isRequired,
    onChange: PropTypes.func.isRequired,
 };

 static defaultProps = {
   order: 'none',
   initialOrder: 'desc',
 };

 onClick = () => {
   const { onChange, initialOrder, order, dataKey } = this.props;

   if (dataKey) {
     let nextOrder = 'none';

     if (order === 'none') {
       nextOrder = initialOrder;
     } else if (order === 'desc') {
       nextOrder = 'asc';
     } else if (order === 'asc') {
       nextOrder = 'desc';
     }

     onChange({ orderBy: dataKey, order: nextOrder });
   }
 };

 render() {
   const { name, order, hasRate, rateType } = this.props;

   return (
     <div className="sortable-th" onClick={this.onClick}>
       <span>{name}</span>
       <SortIcon order={order} />
     </div>
   );
 }
}

經過這個例子能夠看到,thtd 接收 ReactElement 類型的 props 可以讓外部很好的控制單元格的內容,每一個單元格不僅是接收 data 數據的封閉單元。

總結

總結一些本身的感想:

  • 前端工程師也須要往前走一步,瞭解用戶習慣。在寫這個組件以前,我一直是用 ul 來寫表格的,用 ul 寫的表格調整樣式比較便利,後來發現用戶不少時候喜歡把整個表格裏面的內容 copy 下來用於存檔。然而,ul 寫的表格 copy 後粘貼在 excel 中,整行的內容都在一個單元格里面,用 table 寫的表格則可以幾乎保持本來的格式,因此咱們此次用了原生的 table 來寫表格。

  • 業務代碼中組件抽取的粒度一直是一個比較糾結的問題。粒度太粗,項目成員之間須要寫不少重複的代碼。粒度太細,後續可擴展性又很低,因此只能是你們根據業務特色來評估了。像 Table 這樣的組件很是通用,並且後續確定有新的類型冒出來,因此粒度不宜太細。固然,咱們這樣寫 Table 組件後,你們能夠抽取經常使用的一些 XXXThXXXTd

最終,我把此次 Table 組件的經驗抽離出來,開源到 https://github.com/recharts/react-smart-table,但願開發者們能夠參考。

相關文章
相關標籤/搜索