琼玖

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. eclipse导入项目,项目名出现红叉的情况(修改版)

    转至:http://blog.csdn.net/niu_hao/article/details/17440247 今天用eclipse导入同事发给我的一个项目之后,项目名称上面出现红叉,但是其他地方都 ...

  2. python16_day04【编码、函数、装饰器、包】

    一.编码总结 """python2 文件存储默认是ascii方式,启动加#coding:utf8就是文件以utf8方式打开.否则就是以ascii.变量则是str. 例子: ...

  3. ptyhon从入门到放弃之操作系统基础

    *2.操作系统操作系统基础1.什么是操作系统操作系统就是一个协调.管理和控制计算机硬件和软件的控制程序.2.为何要有操作系统现代的计算机系统主要是由一个或者多个处理器,主存,硬盘,键盘,鼠标,显示器, ...

  4. HDU 6351 (Beautiful Now) 2018 Multi-University Training Contest 5

    题意:给定数N(1<=N<=1e9),k(1<=k<=1e9),求对N的任意两位数交换至多k次能得到的最小与最大的数,每一次交换之后不能出现前导零. 因为N最多只有10位,且给 ...

  5. LeetCode: Next Greater Element I

    stack和map用好就行 public class Solution { public int[] nextGreaterElement(int[] findNums, int[] nums) { ...

  6. ExtJS + fileuploadfield实现文件上传

    后台服务端接收文件的代码: /** * 后台上传文件处理Action */ @RequestMapping(value = "/uploadFile", method=Reques ...

  7. hadoop07---synchronized,lock

    synchronized 锁是jvm控制的,控制锁住的代码块只能有一个线程进入.线程执行完了锁自动释放,抛出异常jvm会释放锁. synchronized的缺陷 1.如果一个线程被阻塞了,其余的线程 ...

  8. C#基础--应用程序域(Appdomain)

    AppDomain理解 为了保证代码的键壮性CLR希望不同服务功能的代码之间相互隔离,这种隔离可以通过创建多个进程来实现,但操作系统中创建进程是即耗时又耗费资源的一件事,所以在CLR中引入了AppDo ...

  9. 当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 ...

  10. Oracle数据类型(4)

    字符类型: CHAR(size):固定长度字符串,最大长度2000 bytes VARCHAR2(size):可变长度的字符串,最大长度4000 bytes,可做索引的最大长度749 NCHAR(si ...