React 实现一个漂亮的 Table
概述
对于企业级后台产品来说,Table 应该是使用最频繁的组件了,它通常比 Form 和 Chart 的使用还频繁。对于这么一个常用的组件,我们决定要把它从 RSuite 中单独出来开发,并且要具有一定的通用性,适应很多场景。 首先看一下,Table 完成的效果。
- 预览地址: https://rsuitejs.com/rsuite-table
- Github: https://github.com/rsuite/rsuite-table
最开始促使我们去实现这个 Table 组件是因为产品经理希望表格可以像 Excel一样固定表头和列,我们都知道 HTML table 是不支持这个功能,但是在实际应用中,对于数据行多列多的情况下,固定表头和列非常有用,方便数据关联浏览。我们的组件库都是 React 的, 开源环境中也没有找到一个适合的我们的 Table 组件。 Ant Design 中的 Table 估计有些人用过,UI 比较漂亮,但是在固定表头和列的这个功能上我还是有些不满意,特别是要同时固定表头和列的时候,在 Retina 屏幕上,Ant Table 通过触摸板滚动表格,固定的区域和非固定的区域会对不整齐,看上去会抖动,这个体验不是特别好。我知道 facebook 的 FixedDataTable 针对这块的处理做的还不错,是一个好的参考,特别是大数据量渲染也不卡顿,但是有些功能也不能满足我们的业务场景,比如在要 Table 中呈现一个树形结构就没有这个功能。所以还是决定自己造这个轮子。
设计
在 UI 的设计上符合 RSuite 的整体风格。 我们具体看一下组件的设计,整个 Table 提供了 5 个组件,分别是:
<Table>
定义表格,可以设置数据源,表格类型等等<Column>
定义列,设置列与数据源关联的 key, 设置列宽度,设置是否可以排序,是否需要固定列等等。<Cell>
定义单元格,用于渲染数据的组件,可以自定义显示的方式。<HeaderCell>
定义列头的单元格。<TablePagination>
定义分页,是一个可选组件。
看一个简单的示例:
npm i rsuite rsuite-table --save
有些地方依赖了 RSuite 中的基础组件,所有需要安装
rsuite
。
import { Table, Column, HeaderCell, Cell } from 'rsuite-table';
<Table data={data} >
<Column width={100} sort fixed resizable>
<HeaderCell>ID</HeaderCell>
<Cell dataKey="id" />
</Column>
<Column width={100} sort resizable>
<HeaderCell>Name</HeaderCell>
<Cell dataKey="name" />
</Column>
<Column width={100} sort resizable>
<HeaderCell>Email</HeaderCell>
<Cell dataKey="email" />
</Column>
</Table>
这是一个简单 3 列的表格,接下来我们来看一下具体的功能点。
功能介绍
锁定列
表头是默认固定的不需要额外的配置,要固定列,需要在固定的列 <Column>
添加 fixed
属性。
<Column width={100} fixed>
<HeaderCell>ID</HeaderCell>
<Cell dataKey="id" />
</Column>
这个功能是所有功能里面最麻烦的,特别是表头和列同时固定的时候,前面我也提到过 Ant Design 的 Table 就存在一个问题,滚动的时候固定列和非固定列未对齐,以下是一个 Ant Design 的 Table 的一个截图和访问链接。
访问地址: https://ant.design/components/table-cn/#components-table-demo-fixed-columns-header
造成这个问题的主要原因是 onScroll
触发的频率和渲染的速度跟不上造成的, 如果要列和表头都固定,那必然会在一个方向上需要手动修改元素的位置,这里肯定不能用 React state 存储位置,然后等待渲染,那太慢了。所有需要操作 DOM, 去改变元素的位置,这里有这几个需要注意的技术点:
- 用 transform: translate3D 代替 top 与 left ,因为 top/left 会导致回流,而 translate 只产生重绘,性能会更好,另外 translate3D 走的是 3D, 在手机浏览器器上会 GPU 加速。
onScroll
触发的频率和渲染的速度会存在跟不上的情况,所有这里最好是自己实现一个滚动条,在 Table Body 上监听onWheel
事件,在滚动条上监听onMouse*
事件。 在自己实现滚动条的时候需要注意的是,在 Mac 的 chrome 上,左右滑动的时候会触发浏览器的上一页和下一页功能,所以这里的事件冒泡要处理好(本来想找一个开源的滚动条轮子,发现有好多组件这个问题没有处理好,所以就自己写了)。
对 DOM 操作用到了 dom-lib
我们的 Table 在处理上面两点以后,就解决了 Ant Design 的 Table 滚动存在的问题,当然如果大家有更好的方案,感谢你分享一下。 另外,Ant Table 有很多方面做得是比我们好的,比如它支持固定右侧的列,支持嵌套表格等等功能。
可调整列宽
在表格中有些列的数据有长有短,不太好预测,但还是希望在一个单元格内显示,如果给列固定好一个宽度以后,那超出单元格的内容就会被截断隐藏,导致信息显示不完整。Excel 的列是可以调整宽度的,所以我们也希望列可以调整宽度,只需要在 <Column>
设置一个 resizable
属性。
<Column width={130} sortable>
<HeaderCell>First Name</HeaderCell>
<Cell dataKey="firstName" />
</Column>
自动设置列宽
有一种情况,Table 在页面中的宽度比如是 1000px
+ (可能更宽,根据显示器屏幕的宽度决定), 但是这个 Table 只有 3 列,如果每列都固定一个 200px
, 肯定 撑不满整个 Table,导致不美观, 我们都知道 HTML table, 当给 table 设置 width:100%
以后,列会根据内容自动撑满,如果给其中一个 td 设置了 width
, 那 Table 剩下的 width, 会被剩下的几列撑满。那在 rsuite-table 怎么解决问题呢? 看以下示例:
<Table width={1000}>
<Column width={100}>
<HeaderCell>First Name</HeaderCell>
<Cell dataKey="firstName" />
</Column>
<Column flexGrow={1}>
<HeaderCell>City</HeaderCell>
<Cell dataKey="city" />
</Column>
<Column flexGrow={2}>
<HeaderCell>Company Name</HeaderCell>
<Cell dataKey="companyName" />
</Column>
</Table>
在 <Column>
组件上提供了一个 flexGrow
属性,有点类似 CSS3 中的 flex-grow
属性。上面示例中,Table 的 width
为 1000
, 第一列的 width:100
, 第二列设置为 flexGrow:1
, 第三列设置为 flexGrow:2
。 渲染后计算的结果是:
- 第一列:
100px
- 第二列:
flexGrow:1
,(1000 - 100)/(2 + 1) * 1
=300px
- 第三列:
flexGrow:2
,(1000 - 100)/(2 + 1) * 2
=600px
排序
排序是一个基础的功能,在需要排序的列 <Column>
设置一个 sortable
属性。 同时在 <Table>
定义一个 onSortColumn:Function
回调函数,点击列头排序图标的时候,会触发该方法,并返回 sortColumn:String
和 sortType:String('asc'|desc)
。 看一下示例:
<Table
onSortColumn={(sortColumn, sortType)=>{
console.log(sortColumn, sortType);
}}
>
<Column width={50} sortable>
<HeaderCell>Id</HeaderCell>
<Cell dataKey="id" />
</Column>
<Column width={130} sortable >
<HeaderCell>First Name</HeaderCell>
<Cell dataKey="firstName" />
</Column>
<!--... -->
</Table>
分页
提供了一个 <TablePagination>
组件,用于显示分页栏,这里的分页需要开发人员自己去处理数据,看一下示例代码:
function formatLengthMenu(lengthMenu) {
return (
<div className="table-length">
<span> 每页 </span>
{lengthMenu}
<span> 条 </span>
</div>
);
}
function formatInfo(total, activePage) {
return (
<span>共 <i>{total}</i> 条数据</span>
);
}
<TablePagination
formatLengthMenu={formatLengthMenu}
formatInfo={formatInfo}
displayLength={100}
total={500}
onChangePage={this.handleChangePage}
onChangeLength={this.handleChangeLength}
/>
- formatLengthMenu 格式化显示行数;
- formatInfo 格式化显示总条目信息;
- displayLength 默认显示多少行数据,可以通过 state 管理;
- total 它不是当前返回数据的行数,他是所有数据的总条目数,这个需要后端 API 的返回,通过这个值与displayLength,才能计算出表格分多少页。可以通过 state 管理;
- onChangePage 切换分页的回调函数;
- onChangeLength 切换显示条目数的回调函数。
看一下,效果:
树形表格
先看一下树形表格的样子
渲染成树形的表格需要设置两个地方,首先 <Table>
组件上设置一个 isTree
属性,同时 data
中的数据需要通过 children
来定义关系结构。
<Table data={data} isTree expand height={400}>
data 中的数据结构
[{
labelName: '汽车',
status: 'ENABLED',
children: [
{
labelName: '梅赛德斯-奔驰',
status: 'ENABLED',
count: 460
}
...
]
...
}]
自定义单元格
单元格中的内容往往需要能交互的,比如设置为一个连接,或者 hover
的时候能显示一段信息等等。 在 rsuite-table 中,可以对 Cell
进行自定义。 先看一下以下是一个自定义后的表格图例:
比如,显示一个图片,定义一个 ImageCell
组件:
const ImageCell = ({ rowData, dataKey, ...props }) => (
<Cell {...props}>
<img src={rowData[dataKey]} width="50" />
</Cell>
);
用的时候:
<Column width={200} >
<HeaderCell>Avartar</HeaderCell>
<ImageCell dataKey="avartar" />
</Column>
比如,要格式化日期,就定义一个 DateCell
组件:
const DateCell = ({ rowData, dataKey, ...props }) => (
<Cell {...props}>
{rowData[dataKey].toLocaleString()}
</Cell>
);
用的时候:
<Column width={200} >
<HeaderCell>Action</HeaderCell>
<DateCell dataKey="date" />
</Column>
自定义行高
如果在实际应用中需要根据数据内容来定义行高,可以使用以下方式
<Table
onRerenderRowHeight={(rowData) => {
if (rowData.firstName === 'Janis') {
return 30;
}
}}
>
...
</Table>
可编辑的表格
可编辑的表格,只需要自定义一个 <Cell>
, 然后通过 state
管理状态。
export const EditCell = ({ rowData, dataKey, onChange, ...props }) => {
return (
<Cell {...props}>
{rowData.status === 'EDIT' ? (
<input
className="input"
defaultValue={rowData[dataKey]}
onChange={(event) => {
onChange && onChange(rowData.id, dataKey, event.target.value);
}}
/>
) : rowData[dataKey]}
</Cell>
);
};
遗留的问题
- 内容自动换行,并且自动设置行高,在 HTML table 中很容易实现这个功能,如果整个
<Table>
是的通过 CSS 布局控制也许能实现这个功能,但是在实现的时候,很多地方都是通过 JS 控制高度,比如: 行高、单元格的 left,top 相对位置等等,所以要根据内容来自动行高是比较麻烦的事情,暂时没想到好的解决办法,但是我们提供了一个onRerenderRowHeight
函数,可以让用户自己根据内容来控制行高。 - 根据内容自动设置列宽, 这个问题暂时也没有想到好的解决方案, 现在只能通过
flexGrow
来填充剩余宽度。 - 固定列在右侧,这个功能后续会考虑加进去。
- 表头分组,合并单元格,这个功能麻烦点在于,我们所有的列都是可以调整列宽的,如果同时考虑合并单元格逻辑上处理有些麻烦,不过后续会考虑加入该功能。
如果,你对这些问题有好的想法欢迎你 提交 pull request。 如果,你在使用中存在任何问题,可以提交 issues。
React 实现一个漂亮的 Table的更多相关文章
- 一个漂亮的输出MySql数据库表结构的PHP页面
经常为了方便和直观,我们会首先直接在数据库中设计出表,但是接下来又要将表的结构和设计编写在设计文档中,以便编码的时候可以直观的查询,一旦数据库表非常多,字段非常多的时候,这无疑是件非常郁闷的工作. 这 ...
- 一个漂亮的JavaScript“警告”替代品
下载 一个漂亮的JavaScript"警告"替代品 安装 $ npm安装-节省sweetalert 使用 从"sweetalert"进口swal; 横波测井(& ...
- ctex moderncv版本更新--用latex写一个漂亮的简历
我的电脑是win7系统32位,ctex版本是v2.9.2.164 full(http://www.ctex.org/CTeXDownload) 一直不太清楚moderncv里面类似\cventry这种 ...
- PS网页设计教程XXIV——从头设计一个漂亮的网站
作为编码者,美工基础是偏弱的.我们可以参考一些成熟的网页PS教程,提高自身的设计能力.套用一句话,“熟读唐诗三百首,不会作诗也会吟”. 本系列的教程来源于网上的PS教程,都是国外的,全英文的.本人尝试 ...
- 分享一个漂亮的ProgressBar控件
codeprject上看到的一个漂亮的ProgressBar控件.是用vb.net开发的. C#直接在工具箱中引用即可. 地址:http://www.codeproject.com/Articles/ ...
- 小强的HTML5移动开发之路(5)——制作一个漂亮的视频播放器
来自:http://blog.csdn.net/dawanganban/article/details/17679069 在前面几篇文章中介绍了HTML5的特点和需要掌握的基础知识,下面我们开始真正的 ...
- 一个漂亮的php验证码类
一个漂亮的php验证码类(分享) 作者: 字体:[增加 减小] 类型:转载 下面小编就为大家分享一个漂亮的php验证码类.需要的朋友可以过来参考下 直接上代码: 复制代码 代码如下: //验证 ...
- 用react编写一个hello world
我要分享的是用react搭建一个简单的hello world, 一个小demo, 大神请略过 首先看一下目录结构 创建一个目录, 用于存放demo mkdir reactHello cd reactH ...
- 4-13 Webpacker-React.js; 用React做一个下拉表格的功能: <详解>
Rails5.1增加了Webpacker: Webpacker essentially is the decisions made by the Rails team and bundled up i ...
随机推荐
- NOIP模拟:饼干(简单规律推导)
题目描述 小美有一张很大的网格:2 n * 2 n .每次小美会选一个小矩阵 2 x * 2 x , x > 0,小矩阵不能超过网格的边界.然后把右上一半都放上饼干.下图是当 x=1或2 的时候 ...
- (转)ZXing解析二维码
1 ZXing解析二维码 上一篇文件已经说过如何用ZXing进行生成二维码和带图片的二维码,下面说下如何解析二维码 二维码的解析和生成类似,也可以参考google的一个操作类 BufferedImag ...
- Framework7 索引列表插件的问题
前言 Framework7 作为移动端的开发框架的优良之处已经无需多言.现在已经有了 React 和 Vue 版本,之前在项目中用过 F7 + vue 的开发方式,无论是效率还是产出都近乎完美.有时间 ...
- ES2017中的async函数
前面的话 ES2017标准引入了 async 函数,使得异步操作变得更加方便.本文将详细介绍async函数 概述 async 函数是 Generator 函数的语法糖 使用Generator 函数,依 ...
- spring boot 整合mybatis + swagger2
之前使用springMVC+spring+mybatis,总是被一些繁琐的xml配置,有时候如果配置出错,还要检查各种xml配置,偶然接触到了spring boot 后发现搭建一个web项目真的是1分 ...
- Java Web开发中Spring+MyBatis框架的简单搭建
这里使用的eclipse,首先创建一个动态web项目. 1.导入Spring IOC.AOP.DAO.dbcp.dbdrive.mybatis.jar . mybatis-spring.jar 本人 ...
- Python 发展历史
1. Python发展历史 起源 Python的作者,Guido von Rossum,荷兰人.1982年,Guido从阿姆斯特丹大学获得了数学和计算机硕士学位.然而,尽管他算得上是一位数学家,但他更 ...
- iframe嵌入页面不能全部展示
在嵌入页面不能全部展示的问题中,可以通过js改变iframe的高度 html部分代码: <iframe src="#" name="i" id=" ...
- 安徽省2016“京胜杯”程序设计大赛_H_单身晚会
单身晚会 Time Limit: 1000 MS Memory Limit: 65536 KB Total Submissions: 53 Accepted: 16 Description ZJ和Z ...
- [算法题] 3Sum
题目内容 题目来源:LeetCode Given an array S of n integers, are there elements a, b, c in S such that a + b + ...