zoukankan      html  css  js  c++  java
  • React 实现 Table 的思考

    React 实现 Table 的思考

     

    琼玖琼玖
    1 年前 (写的零零散散, 包括github不怎么样)

    Table 是最常用展示数据的方式之一,可是一个产品中往往很多非常类似的 Table,

    但是我们碰到的情况往往是 Table A 要排序,Table B 不需要排序,等等这种看起来非常类似,

    但是又不完全相同的表格。这种情况下,到底要不要抽取一个公共的 Table 组件呢 ( 懒一点, 不抽, 配置配资)?对于这个问题,

    我们团队也纠结了很久,先后开发了多个版本的 Table 组件,在最近的一个项目中,

    产出了第三版 Table 组件,能够较好的解决灵活性和公共逻辑抽取的问题。

    本文将会详细的讲述这种 Table 组件解决方案产出的过程和一些思考。

    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>
      );
    }
    

    这种实现方法带来的问题是:

    • 每次写表格需要写很多布局类的样式

    • 重复代码很多,而且项目成员之间很难达到统一,A 可能喜欢用表格来布局,B 可能喜欢用 ul 来布局

    • 相似但是不完全相同的表格 很难复用

    抽象过程

    组件是对数据和方法的一种封装,在封装之前,我们总结了一下表格型的展示的特点

    • 输入数据源较统一,一般为对象数组

    • 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} />
    

    其中 onRenderTd 和 onRenderTh 分别是渲染 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 添加了 width 和 align 属性。

    加这两个属性的原因很容易想到,因为我们在写表格相关业务时,

    样式里面写的最多的就是单元格的宽度和对齐方式。我们来看一下 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 的类型就更复杂了,不仅能够接收 function 和 ReactElement 这两种类型,还

    有 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>
      );
    }
    

    代码说明了一切,就不再详细说了。当然,在业务组件里,还可以加上公共的错误处理逻辑。

    单元格示例

    前面提到我们的 td 和 th 还可以接收 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>
       );
     }
    }
    

    通过这个例子可以看到,th 和 td 接收 ReactElement 类型的 props 能够让外部很好的控制单元格的内容

    每个单元格不只是接收 data 数据的封闭单元

    总结

    总结一些自己的感想:

    • 前端工程师也需要往前走一步,了解用户习惯。在写这个组件之前,我一直是用 ul 来写表格的,

    • 用 ul 写的表格调整样式比较便利,后来发现用户很多时候喜欢把整个表格里面的内容 copy 下来用于存档。

    • 然而,ul 写的表格 copy 后粘贴在 excel 中,整行的内容都在一个单元格里面

    • 用 table 写的表格则能够几乎保持原本的格式,所以我们这次用了原生的 table 来写表格。

    • 业务代码中组件抽取的粒度一直是一个比较纠结的问题。粒度太粗,项目成员之间需要写很多重复的代码。

    • 粒度太细,后续可扩展性又很低,所以只能是大家根据业务特点来评估了

    • 像 Table 这样的组件非常通用,而且后续肯定有新的类型冒出来,所以粒度不宜太细。

    • 当然,我们这样写 Table 组件后,大家可以抽取常用的一些 XXXTh 和 XXXTd。

    最终,我把这次 Table 组件的经验抽离出来,

    开源到 GitHub - recharts/react-smart-table: A smart table component.,希望开发者们可以参考

  • 相关阅读:
    CSS 实现隐藏滚动条同时又可以滚动
    手机端自适应布局demo
    手机端自适应布局demo
    手机端自适应布局demo
    七个帮助你处理Web页面层布局的jQuery插件
    一笔画问题
    数组模拟邻接表
    邻接矩阵存图
    BFS 遍历图
    DFS 遍历图
  • 原文地址:https://www.cnblogs.com/dhsz/p/6862258.html
Copyright © 2011-2022 走看看