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,