琼玖

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

React 实现 Table 的思考的更多相关文章

  1. React.js入门笔记(续):用React的方式来思考

    本文主要内容来自React官方文档中的"Thinking React"部分,总结算是又一篇笔记.主要介绍使用React开发组件的官方思路.代码内容经笔者改写为较熟悉的ES5语法. ...

  2. react.js table组件【可以直接使用】

    最近在做一个CMS,使用的技术是刚刚学习的react.js,准备制作一个查询的页面以及一个新增的页面. 这是table的公共组件: 我们在使用的过程中,只会用到: 制作出来的查询页面: 新增页面: 上 ...

  3. react 组装table列表带分页

    2.组装编辑界面 /** * Created by hldev on 17-6-14. */ import React, {Component} from "react"; imp ...

  4. React ant table 用 XLSX 导出excel文件

    近期做了一个react ant design 的table转换成excel 的功能 总结下 首先我们会自己定义下 antdesign 的table的columns其中有可能有多语言或者是render方 ...

  5. react项目组件化思考

    三个原则 single store render from top immutable data single store,便于组件之间通信. render from top,因为store就一个,每 ...

  6. 围绕react衍生出来的思考

    优势一.声明式开发 首先react是声明式的开发方式,这个与之对应的是命令式开发方式,之前在用jquery写代码的时候,都是直接来操作dom,直接操作dom的这种编程方式,我们把他叫做命令式的编程,也 ...

  7. react antd Table动态合并单元格

    示例数据 原始数组 const data = [ { key: '0', name: 'John Brown', age:22, address: 'New York No. 1 Lake Park' ...

  8. react中使用antd Table组件滚动加载数据的实现

    废话不多说,直接上代码.一目了然. import React, { Component } from "react"; import { Table } from "an ...

  9. React 快速入门小记

    大约半个月前,我一直在思考一个问题,Angular.React 和 Vue,究竟该学什么? 听取了几位前辈的意见,也综合考虑了各方面的原因,最终选择了 React,希望我"没有选错" ...

随机推荐

  1. Mark一下 mysql 误删除root用户的解决方法

    今天学习mysql用户管理,不小心将mysql.user表中的root用户给删掉了,然后就无法登录mysql了,网上找到了linux下的解决方法,我做了简单的修改,改成了我的windows版,恢复方法 ...

  2. Poj 1755Triathlon 未Ac,先mark

    地址:http://poj.org/problem?id=1755 题目: Triathlon Time Limit: 1000MS   Memory Limit: 10000K Total Subm ...

  3. SQL语句2

    1. SELECT * FROM Persons WHERE City NOT LIKE '%lon%' 2. SELECT * FROM Persons WHERE FirstName LIKE ' ...

  4. bat脚本相关

    前期准备: 将要执行的脚本名字生成到一个txt文件 首先进入dos运行程序的目录下:输入dir *.jmx /B>FileScript.txt 采用dir *.jmx>list.txt 如 ...

  5. python图片文字识别笔记

    我的环境为python3 坑比较多,在此做记录,以备查阅 命令行安装: pip install PIL pip install pytesseract pip install Pillow 下载tes ...

  6. 2-Color Dutch National Flag Problem

    2-Color Dutch National Flag Problem 问题 a[0..n-1]中包含红元素或蓝元素;重新放置使得 红元素均在蓝元素之前. 循环不变式 每一次循环,a[0...k-1] ...

  7. 正则表达式【TLCL】

    grep[global regular expression print] print lines matching a pattern grep [options] regex [file...] ...

  8. 还在纠结注册.com域名还是.cn域名?

    一.概念 .com域名,国际最广泛流行的通用域名格式.国际化公司都会注册. .com域名:当然也可以选择.net/.org以.com为结尾的国际域名. 例如表示工商企业的 .com. 同时还有 .ne ...

  9. java中Vector类的常用方法

    Vector类是实现List接口,所以继承的方法就不在这里讲了 https://www.cnblogs.com/xiaostudy/p/9503199.html public void add(int ...

  10. codeforces528D Fuzzy Search

    本文版权归ljh2000和博客园共有,欢迎转载,但须保留此声明,并给出原文链接,谢谢合作. 本文作者:ljh2000 作者博客:http://www.cnblogs.com/ljh2000-jump/ ...