琼玖

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. Hbase 学习笔记2----概念

    说在前面,本文部分内容来源于社区官网经过适度翻译,部分根据经验总结,部分是抄袭网络博文,(不一一列举引用,在此致歉)一并列在一起,本文的目的,希望能总结出一些有用的,应该注意到的东西,基本思路是先提出 ...

  2. 连接postgresql

    # psycopg2 engine=create_engine('postgresql+psycopg2://scott:tiger@localhost/mydatabase')#  python 连 ...

  3. Typecho部署安装

    此文章已经在这里上. 如果您看到这篇文章,表示您的 blog 已经在digitalocean.com安装成功.下面说下安装的步骤,此文章都是在digitalocean.com的centos上成功安装: ...

  4. cdoj1325卿学姐与基本法

    地址:http://acm.uestc.edu.cn/#/problem/show/1325 题目: 卿学姐与基本法 Time Limit: 2000/1000MS (Java/Others)     ...

  5. Spring @Qualifier l转

    当候选 Bean 数目不为 1 时的应对方法 在默认情况下使用 @Autowired 注释进行自动注入时,Spring 容器中匹配的候选 Bean 数目必须有且仅有一个.当找不到一个匹配的 Bean ...

  6. 【Head First Servlets and JSP】笔记1

    1.把Java放到HTML中,JSP应运而生. 2.Servlet本身并没有main()方法,所以必须要有其他Java程序去调用它,这个Java程序就是Web容器(Container).Tomcat就 ...

  7. Spring中的@Transactional以及事务的详细介绍

    首先来说下事务,说到事务就不得不说它的四个特性(acid): 一.特性 1.原子性(atomicity):一个事务当作为一个不可分割的最小工作单元,一组操作要么全部成功,要么全部失败. 2.一致性(c ...

  8. jvm-java内存模型与锁优化

    java内存模型与锁优化 参考: https://blog.csdn.net/xiaoxiaoyusheng2012/article/details/53143355 https://blog.csd ...

  9. Pow,求x的y次幂

    算法分析:很显然用递归.但是直接用递归会造成栈溢出,时间复杂度是o(n).所以要用分治思想,时间复杂度是o(logN). public class Power { //栈溢出,时间复杂度是o(n) p ...

  10. mysql desc esc 基本命令总结

    asc 按升序排列desc 按降序排列 下列语句部分是Mssql语句,不可以在access中使用. SQL分类:DDL—数据定义语言(CREATE,ALTER,DROP,DECLARE)DML—数据操 ...