React 实现 Table 的思考

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: () => {} },name: 'col-b',onRenderTh: () => {},];

<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,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,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),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,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,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,希望开发者们可以参考。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


react 中的高阶组件主要是对于 hooks 之前的类组件来说的,如果组件之中有复用的代码,需要重新创建一个父类,父类中存储公共代码,返回子类,同时把公用属性...
我们上一节了解了组件的更新机制,但是只是停留在表层上,例如我们的 setState 函数式同步执行的,我们的事件处理直接绑定在了 dom 元素上,这些都跟 re...
我们上一节了解了 react 的虚拟 dom 的格式,如何把虚拟 dom 转为真实 dom 进行挂载。其实函数是组件和类组件也是在这个基础上包裹了一层,一个是调...
react 本身提供了克隆组件的方法,但是平时开发中可能很少使用,可能是不了解。我公司的项目就没有使用,但是在很多三方库中都有使用。本小节我们来学习下如果使用该...
mobx 是一个简单可扩展的状态管理库,中文官网链接。小编在接触 react 就一直使用 mobx 库,上手简单不复杂。
我们在平常的开发中不可避免的会有很多列表渲染逻辑,在 pc 端可以使用分页进行渲染数限制,在移动端可以使用下拉加载更多。但是对于大量的列表渲染,特别像有实时数据...
本小节开始前,我们先答复下一个同学的问题。上一小节发布后,有小伙伴后台来信问到:‘小编你只讲了类组件中怎么使用 ref,那在函数式组件中怎么使用呢?’。确实我们...
上一小节我们了解了固定高度的滚动列表实现,因为是固定高度所以容器总高度和每个元素的 size、offset 很容易得到,这种场景也适合我们常见的大部分场景,例如...
上一小节我们处理了 setState 的批量更新机制,但是我们有两个遗漏点,一个是源码中的 setState 可以传入函数,同时 setState 可以传入第二...
我们知道 react 进行页面渲染或者刷新的时候,会从根节点到子节点全部执行一遍,即使子组件中没有状态的改变,也会执行。这就造成了性能不必要的浪费。之前我们了解...
在平时工作中的某些场景下,你可能想在整个组件树中传递数据,但却不想手动地通过 props 属性在每一层传递属性,contextAPI 应用而生。
楼主最近入职新单位了,恰好新单位使用的技术栈是 react,因为之前一直进行的是 vue2/vue3 和小程序开发,对于这些技术栈实现机制也有一些了解,最少面试...
我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel 处理后的 type 类型,最后还是要处理虚拟 dom。本小节我们学习下组件的更新机...
前面几节我们学习了解了 react 的渲染机制和生命周期,本节我们正式进入基本面试必考的核心地带 -- diff 算法,了解如何优化和复用 dom 操作的,还有...
我们在之前已经学习过 react 生命周期,但是在 16 版本中 will 类的生命周期进行了废除,虽然依然可以用,但是需要加上 UNSAFE 开头,表示是不安...
上一小节我们学习了 react 中类组件的优化方式,对于 hooks 为主流的函数式编程,react 也提供了优化方式 memo 方法,本小节我们来了解下它的用...
开源不易,感谢你的支持,❤ star me if you like concent ^_^
hel-micro,模块联邦sdk化,免构建、热更新、工具链无关的微模块方案 ,欢迎关注与了解
本文主题围绕concent的setup和react的五把钩子来展开,既然提到了setup就离不开composition api这个关键词,准确的说setup是由...
ReactsetState的执行是异步还是同步官方文档是这么说的setState()doesnotalwaysimmediatelyupdatethecomponent.Itmaybatchordefertheupdateuntillater.Thismakesreadingthis.staterightaftercallingsetState()apotentialpitfall.Instead,usecom