资讯专栏INFORMATION COLUMN

React 实现 Table 的思考

ChanceWong / 945人阅读

摘要:加这两个属性的原因很容易想到,因为我们在写表格相关业务时,样式里面写的最多的就是单元格的宽度和对齐方式。然而,写的表格后粘贴在中,整行的内容都在一个单元格里面,用写的表格则能够几乎保持原本的格式,所以我们这次用了原生的来写表格。

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 (
    
    { columnOpts.map((opt, colIndex) => (
  • {opt.name}
  • )) }
    { data.map((entry, rowIndex) => (
  • { columnOpts.map((opt, colIndex) => ( {entry[opt.key]} )) }
  • )) }
); }

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

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

重复代码很多,而且项目成员之间很难达到统一,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: () => {} },
];

其中 onRenderTdonRenderTh 分别是渲染 td 和 th 时的回调函数。

到这里我们发现对于稍微复杂一点的 table,columnOpts 将会是一个非常大的配置数组,我们有没有办法不使用数组来维护这些配置呢?这里我们想到的一个办法是创建一个 Column 的组件,让大家可以这么来写这个 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 (
      
        {content}
      
    );
  });
};

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 (
      
        {content}
      
    );
  });
};

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

  return data.map((entry, index) => {
    return (
      
        {renderTds(data, entry, columns, index)}
      
    );
  });
};

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

  return (
    
{hasNames(columns) && ( {renderThs(columns)} )} {renderRows(data, columns)}
); }

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

单元格示例

前面提到我们的 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 (
     
{name}
); } }

通过这个例子可以看到,thtd 接收 ReactElement 类型的 props 能够让外部很好的控制单元格的内容,每个单元格不只是接收 data 数据的封闭单元。

总结

总结一些自己的感想:

前端工程师也需要往前走一步,了解用户习惯。在写这个组件之前,我一直是用 ul 来写表格的,用 ul 写的表格调整样式比较便利,后来发现用户很多时候喜欢把整个表格里面的内容 copy 下来用于存档。然而,ul 写的表格 copy 后粘贴在 excel 中,整行的内容都在一个单元格里面,用 table 写的表格则能够几乎保持原本的格式,所以我们这次用了原生的 table 来写表格。

业务代码中组件抽取的粒度一直是一个比较纠结的问题。粒度太粗,项目成员之间需要写很多重复的代码。粒度太细,后续可扩展性又很低,所以只能是大家根据业务特点来评估了。像 Table 这样的组件非常通用,而且后续肯定有新的类型冒出来,所以粒度不宜太细。当然,我们这样写 Table 组件后,大家可以抽取常用的一些 XXXThXXXTd

最终,我把这次 Table 组件的经验抽离出来,开源到 https://github.com/recharts/react-smart-table,希望开发者们可以参考。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/79386.html

相关文章

  • React 导读(三)

    摘要:场景为了更清晰的安排年前年后的工作和值班,现在要对过年期间人员请假的情况进行统计,并且进行一个简单的管理。我们现在来订阅一个名为的事件,用来表示表格中需要展示每条数据。 前言 React 导读(一)React 导读(二) 在之前 2 篇文章中中学习到了写第一个 Web 组件以及常用的生命周期函数的使用,这篇文章将继续之前的目录,开始新的知识点补充: [x] React 如何编写 He...

    zzir 评论0 收藏0
  • 一次完整react hooks实践

    摘要:本次需求其实就两个逻辑输入筛选项。当发生改变时,重新渲染页面首次进入页面时,无任何筛选项。关于的一些,官方也有很棒的文档写在后面本文通过工作中的一个小需求,完成了一次的实践,不过上述代码依然有很多需要优化的地方。 写在前面 showImg(https://segmentfault.com/img/bVbpBgw?w=1000&h=563); 本文首发于公众号:符合预期的CoyPan R...

    kuangcaibao 评论0 收藏0
  • 19年一些微小计划

    摘要:是今年一定要学的东西这两年页面上用的三方组件多了,写的少了,的一些属性不太记得了,针对的学习计划有两个参照的样式进行学习参照的组件样式,学习如何处理样式与组件之间的关系,规范自己的写法。 磕磕绊绊工作有几年了,前端界几乎每天都有新名词,令人眼花缭乱,目瞪狗呆。这两年一直在外包工作,业务写的多些,对js的基础掌握的还不是很到位。最近深感技术嗅觉迟钝,虽然平时也有看书学习,更多的时候都是断...

    harriszh 评论0 收藏0
  • zepto/jQuery、AngularJS、React、Nuclear演化

    摘要:每个框架类库被大量用户大规模使用都说明其戳中了开发者的刚需。但是未执行完的情况下发生人机交互虽然不会报脚本错误,但是严重影响用户体验开发者们被各种爽到之后,这个问题已经被抛到了九霄云外。 写在前面 因为zepto、jQuery2.x.x和Nuclear都是为现代浏览器而出现,不兼容IE8,适合现代浏览器的web开发或者移动web/hybrid开发。每个框架类库被大量用户大规模使用都说明...

    Rindia 评论0 收藏0
  • 教你如何打好根基快速入手react,vue,node

    摘要:谨记,请勿犯这样的错误。由于在之前的教程中,积累了坚实的基础。其实,这是有缘由的其复杂度在早期的学习过程中,将会带来灾难性的影响。该如何应对对于来说,虽然有大量的学习计划需要采取,且有大量的东西需要学习。 前言倘若你正在建造一间房子,那么为了能快点完成,你是否会跳过建造过程中的部分步骤?如在具体建设前先铺设好部分石头?或直接在一块裸露的土地上先建立起墙面? 又假如你是在堆砌一个结婚蛋糕...

    ddongjian0000 评论0 收藏0

发表评论

0条评论

ChanceWong

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<