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: () => {} },
{ 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 的思考的更多相关文章
- React.js入门笔记(续):用React的方式来思考
本文主要内容来自React官方文档中的"Thinking React"部分,总结算是又一篇笔记.主要介绍使用React开发组件的官方思路.代码内容经笔者改写为较熟悉的ES5语法. ...
- react.js table组件【可以直接使用】
最近在做一个CMS,使用的技术是刚刚学习的react.js,准备制作一个查询的页面以及一个新增的页面. 这是table的公共组件: 我们在使用的过程中,只会用到: 制作出来的查询页面: 新增页面: 上 ...
- react 组装table列表带分页
2.组装编辑界面 /** * Created by hldev on 17-6-14. */ import React, {Component} from "react"; imp ...
- React ant table 用 XLSX 导出excel文件
近期做了一个react ant design 的table转换成excel 的功能 总结下 首先我们会自己定义下 antdesign 的table的columns其中有可能有多语言或者是render方 ...
- react项目组件化思考
三个原则 single store render from top immutable data single store,便于组件之间通信. render from top,因为store就一个,每 ...
- 围绕react衍生出来的思考
优势一.声明式开发 首先react是声明式的开发方式,这个与之对应的是命令式开发方式,之前在用jquery写代码的时候,都是直接来操作dom,直接操作dom的这种编程方式,我们把他叫做命令式的编程,也 ...
- react antd Table动态合并单元格
示例数据 原始数组 const data = [ { key: '0', name: 'John Brown', age:22, address: 'New York No. 1 Lake Park' ...
- react中使用antd Table组件滚动加载数据的实现
废话不多说,直接上代码.一目了然. import React, { Component } from "react"; import { Table } from "an ...
- React 快速入门小记
大约半个月前,我一直在思考一个问题,Angular.React 和 Vue,究竟该学什么? 听取了几位前辈的意见,也综合考虑了各方面的原因,最终选择了 React,希望我"没有选错" ...
随机推荐
- eclipse导入项目,项目名出现红叉的情况(修改版)
转至:http://blog.csdn.net/niu_hao/article/details/17440247 今天用eclipse导入同事发给我的一个项目之后,项目名称上面出现红叉,但是其他地方都 ...
- python16_day04【编码、函数、装饰器、包】
一.编码总结 """python2 文件存储默认是ascii方式,启动加#coding:utf8就是文件以utf8方式打开.否则就是以ascii.变量则是str. 例子: ...
- ptyhon从入门到放弃之操作系统基础
*2.操作系统操作系统基础1.什么是操作系统操作系统就是一个协调.管理和控制计算机硬件和软件的控制程序.2.为何要有操作系统现代的计算机系统主要是由一个或者多个处理器,主存,硬盘,键盘,鼠标,显示器, ...
- HDU 6351 (Beautiful Now) 2018 Multi-University Training Contest 5
题意:给定数N(1<=N<=1e9),k(1<=k<=1e9),求对N的任意两位数交换至多k次能得到的最小与最大的数,每一次交换之后不能出现前导零. 因为N最多只有10位,且给 ...
- LeetCode: Next Greater Element I
stack和map用好就行 public class Solution { public int[] nextGreaterElement(int[] findNums, int[] nums) { ...
- ExtJS + fileuploadfield实现文件上传
后台服务端接收文件的代码: /** * 后台上传文件处理Action */ @RequestMapping(value = "/uploadFile", method=Reques ...
- hadoop07---synchronized,lock
synchronized 锁是jvm控制的,控制锁住的代码块只能有一个线程进入.线程执行完了锁自动释放,抛出异常jvm会释放锁. synchronized的缺陷 1.如果一个线程被阻塞了,其余的线程 ...
- C#基础--应用程序域(Appdomain)
AppDomain理解 为了保证代码的键壮性CLR希望不同服务功能的代码之间相互隔离,这种隔离可以通过创建多个进程来实现,但操作系统中创建进程是即耗时又耗费资源的一件事,所以在CLR中引入了AppDo ...
- 当root用户无密码,非超级权限用户时提示mysqladmin: Can't turn off logging; error: 'Access denied; you need the SUPER privilege for this op解决方案
问题: 在centOS上安装了mysql后,卸载了又重新安装,使用mysqladmin -u root password 'new password' 更改密码,提示: mysqladmin: Can ...
- Oracle数据类型(4)
字符类型: CHAR(size):固定长度字符串,最大长度2000 bytes VARCHAR2(size):可变长度的字符串,最大长度4000 bytes,可做索引的最大长度749 NCHAR(si ...