React 下的表格狂想曲

0. 前言

欢迎大家阅读「从零开始的 React 组件开发之路」系列第一篇,表格篇。本系列的特色是从 需求分析、API 设计和代码设计 三个递进的过程中,由简到繁地开发一个 React 组件,并在讲解过程中穿插一些 React 组件开发的技巧和心得。

为什么从表格开始呢?在企业系统中,表格是最常见但功能需求最丰富的组件之一,同时也是基于 React 数据驱动的思想受益最多的组件之一,十分具有代表性。这篇文章也是近期南京谷歌开发者大会前端专场的分享总结。UXCore table 组件 Demo 也可以和本文行文思路相契合,可做参考。

1. 一个简单 React 表格的构造

1.1 需求分析

  • 有表头,每行的展示方式相同,只是数据上有所不同

  • 每一列可能有不同的对齐方式,可能有不同的展示类型,比如金额,比如手机号码等

1.2 API 设计

  • 因为每一列的展示类型不同,因此列配置应该作为一个 Prop,由于有多列应该是一个数组

  • 数据源应该作为基础配置之一,应该作为一个 prop,由于有多行也应该是一个数组

  • 现在的样子:<Table columns={[]} data={[]} />

  • 基本思路是通过遍历列配置来生成每一行

  • data 中的每一个元素应该是一行的数据,是一个 hash 对象。

    {city: '北京',name: '小李'
    }
  • columns 中的每一个元素是一列的配置,也是一个 hash 对象,至少应该包括如下几部分:

    {title: '表头',dataKey: 'city', // 该列使用行中的哪个 key 进行显示
    }
  • 易用性与通用性的平衡

    • 易用性与通用性互相制衡,但并不是绝对矛盾。

    • 何为易用?使用尽量少的配置来完成最典型的场景。

    • 何为通用?提供尽量多的定制接口已适应各种不同场景。

    • 在 API 设计上尽量开放保证通用性

    • 在默认值上提炼最典型的场景提高易用性。

    • 从易用性角度出发

      {align: 'left', // 默认左对齐type: 'money/action', // 提供 'money', 'card', 'cnmobile' 等常用格式化形式delimiter: ',', // 格式化时的分隔符,默认是空格actions: { // 表格中常见的操作列,不以数据进行渲染,只包含动作,hash 对象使配置最简化"编辑": function() {doEdit();}},
      }
    • 从通用性角度出发

      {actions: [ // 相对繁琐,但定制能力更强{title: '编辑',callback: function() {doEdit();},render: function(rowData) {// 根据当前行数据,决定是否渲染,及渲染成定制的样子}}],render: function(cellData, rowData) {// 根据当前行数据,完全由用户决定如何渲染return <span>{`${rowData.city} - ${rowData.name}`}</span>}
      }
    • 提供定制化渲染的两种方式:

      • 渲染函数 (更推荐)

        {render: function(rowData) {return <CustomComp url={rowData.url} />},
        }
      • 渲染组件

        {renderComp: <CustomComp />, // 内部接收 rowData 作为参数
        }
      • 推荐渲染函数的原因:

        1. 函数在做属性比较时,更简单

        2. 约定更少,渲染组件的方式需要配合 Table 预留比如 rowData 一类的接口,不够灵活。

1.3 代码设计

图:Table 的分层设计

图:最初的 Table 结构,详细的分层为后续的功能扩展做好准备。

2. 加入更多的内置功能

目前的表格可以满足我们的最简单常用的场景,但仍然有很多经常需要使用的功能没有支持,如列排序,分页,搜索过滤、常用动作条、行选择和行筛选等。

2.1 需求分析

  • 列排序:升序/降序/默认顺序 Head/Cell 相关

  • 分页:当表格需要展示的条数很多时,分页展示固定的条数 Table/Pagination 相关,这里假设已有 Pagination 组件

  • 搜索过滤:Table 相关

  • 常用操作:Table 相关

  • 行选择:选中某些行,Row/Cell 相关

  • 行筛选:手动展示或者隐藏一些行,不属于任何一列,因此是 Table 级

2.2 API 设计

根据上面对于功能的需求分析,我们很容易定位 API 的位置,完成相应的扩展。

// table 配置,需求对应的模块对应了他的配置在整个配置中的位置
{columns: [ // HEAD/ROW 相关{order: true, // 是否展示排序按钮hidden: false, // 是否隐藏,行筛选需要}],onOrder: function (activeColumn, order) { // 排序时的回调doOrder(activeColumn, order)}, actionBar: { // 常用操作条"打印": function() {doPrint()}, },showSeach: true, // 是否显示搜索过滤,为什么不直接用下面的,这里也是设计上的一个优化点onSearch: function(keyword) { doSearch(keyword) }, // 搜索时的回调showPager: true, // 是否显示分页onPagerChange: function(current, pageSize) {}, // 分页改变时的回调rowSelection: { // 行选择相关onSelect: function(isSelected, currentRow, selectedRows) { doSelect() }}
}
// data 结构
{data: [{city: 'xxx',name: 'xxx',__selected__: true, // 行选择相关,用以标记该行是否被选中,用前后的 __ 来做特殊标记,另一方面也尽可能避免与用户的字段重复}],currentPage: 1, // 当前页数totalCount: 50, // 总条数
}

2.3 代码设计

结构图

图:扩展后的 Table 结构

内部数据的处理

目前组件的数据流向还比较简单,我们似乎可以全部通过 props 来控制状态,制作一个 stateless 的组件。

何时该用 state?何时该用 props?

UI=fn(state, props), 人们常说 React 组件是一个状态机,但我们应该清楚的是他是由 state 和 props 构成的双状态机;

props 和 state 的改变都会触发组件的重新渲染,那么我们使用它们的时机分别是什么呢?由于 state 是组件自身维护的,并不与他的父级组件进行沟通,进而也无法与他的兄弟组件进行沟通,因此我们应该尽量只在页面的根节点组件或者复杂组件的根节点组件使用 state,而在其他情况下尽量只使用 props,这可以增强整个 React 项目的可预知性和可控性。

但凡事不是绝对的,全都使用 Props 固然可以使组件可维护性变强,但全部交给用户来操作会使用户的使用成本大大提高,利用 state,我们可以让组件自己维护一些状态,从而减轻用户使用的负担。

我们举个简单的例子

{/* 受控模式 */}
<input value="a" onChange={ function() {doChange()} } />
{/* 非受控模式 */}
<input onChange={ function() {doChange()} } />

value 配置时,input 的值由 value 控制,value 没有配置时,input 的值由自己控制,如果把 &lt;input /&gt; 看做一个组件,那么此时可以认为 input 此时有一个 state 是 value。显然,无 value 状态下的配置更少,降低了使用的成本,我们在做组件时也可以参考这种模式。

例如在我们希望为用户提供 行选择 的功能时,用户通常是不希望自己去控制行的变化的,而只是关心行的变化时应该拿取的数据,此时我们就可以将 data 这个 prop 变成 state。有一点需要注意的是,用户的 prop

class Table extends React.Component {constructor(props) {super(props);this.data = deepcopy(props.data);this.state = {data: this.data,};}/*** 在 data 发生改变时,更改对应的 state 值。*/componentWillReceiveProps(nextProps, nextState) {if (!deepEqual(nextProps.data, this.data) {this.data = deepcopy(nextProps.data);this.setState({data: this.data,});}}
}

这里涉及的一个很重要的点,就是如何处理一个复杂类型数据的 prop 作为 state。因为 JS 对象传地址的特性,如果我们直接对比nextProps.data 和 this.props.data 有些情况下会永远相等(当用户直接修改 data 的情况下),所以我们需要对这个 prop 做一个备份。

生命周期的使用时机

图:React 的生命周期

  • constructor: 尽量简洁,只做最基本的 state 初始化

  • willMount: 一些内部使用变量的初始化

  • render: 触发非常频繁,尽量只做渲染相关的事情。

  • didMount: 一些不影响初始化的操作应该在这里完成,比如根据浏览器不同进行操作,获取数据,监听 document 事件等(server render)。

  • willUnmount: 销毁操作,销毁计时器,销毁自己的事件监听等。

  • willReceiveProps: 当有 prop 做 state 时,监听 prop 的变化去改变 state,在这个生命周期里 setState 不会触发两次渲染。

  • shouldComponentUpdate: 手动判断组件是否应该更新,避免因为页面更新造成的无谓更新,组件的重要优化点之一。

  • willUpdate: 在 state 变化后如果需要修改一些变量,可以在这里执行。

  • didUpdate: 与 didMount 类似,进行一些不影响到 render 的操作,update 相关的生命周期里最好不要做 setState 操作,否则容易造成死循环。

父子级组件间的通信

父级向子级通信不用多说,使用 prop 进行传递,那么子级向父级通信呢?有人会说,靠回调啊~ onChange等等,本质上是没有错误的,但当组件比较复杂,存在多级结构时,如果每一级都去处理他的子级的回调的话,不仅写起来非常麻烦,而且很多时候是没有意义的。

我们采取的办法是,只在顶级组件也就是 Table 这一层控制所有的 state,其他的各个子层都是完全由 prop 来控制,这样一来,我们只需要 Table 去操作数据,那么我们逐级向下传递一个属于 Table 的回调函数,完成所有子级都只向 Table 做“汇报”,进行跨级通信。

图:父子级间的通信

3. 自行获取数据

3.1 需求分析

作为一个尽可能为用户提高效率的组件,除了手动传入 data 外,我们也应该有自行获取数据的能力,用户只需要配置 url 和相应的参数就可以完成表格的配置,为此我们可能需要以下参数:

  • 数据源,返回的数据格式应和我们之前定义的 data 数据结构一致。 (易用)

  • 随请求一起发出去的参数。(通用)

  • 在发请求前的回调,可以在这里调整发送的参数。(通用)

  • 请求回来后的回调,可以在这里调整数据结构以满足对 data 的要求。(通用)

  • 同时要考虑到内置功能的适配。(易用)

3.2 API 设计

// table 配置,需求对应的模块对应了他的配置在整个配置中的位置
{url: "//fetchurl.com/data", // 数据源,只支持 json 和 jsonpfetchParams: { // 额外的一些参数token: "xxxabxc_sa"},beforeFetch: function(data, from) { // data 为要发送的参数,from 参数用来区分发起 fetch 的来源(分页,排序,搜索还是其他位置)return data; // 返回值为真正发送的参数},afterFetch: function(result) { // result 为请求回来的数据return process(result); // 返回值为真正交给 table 进行展示的数据。},
}

3.3 代码设计

基于前面良好的通信模式,url 的扩展变得非常简单,只需要在所有的回调中加入是否配置 url 的判断即可。

class Table extends React.Component {constructor(props) {super(props);this.data = deepcopy(props.data);this.fetchParams = deepcopy(props.fetchParams);this.state = {data: this.data,};}/*** 获取数据的方法*/fetchData(props, from) {props = props || this.props;const otherParams = process(this.state);ajax(props.url, this.fetchParams, otherParams, from);}/*** 搜索时的回调*/handleSearch(key) {if (this.props.url) {this.setState({searchKey: key,}, () => {this.fetchData();});} else {this.props.onSearch(key);}}componentDidMount() {if (this.props.url) {this.fetchData();}}componentWillReceiveProps(nextProps, nextState) {let newState = {};if (!deepEqual(nextProps.data, this.data) {this.data = deepcopy(nextProps.data);newState['data'] = this.data; }if (!deepEqual(nextProps.fetchParams, this.fetchParams)) {this.fetchParams = deepcopy(nextProps.fetchParams);this.fetchData();}if (nextProps.url !== this.props.url) {this.fetchData(nextProps);}if (Object.keys(newState) !== 0) {this.setState(newState);}}
}

4. 行内编辑

4.1 需求分析

通过双击或者点击编辑按钮,实现行内可编辑状态的切换。如果只是变成普通的文本框那就太 low 了,有追求的我们希望每个列根据数据类型可以有不同的编辑形式。既然是可编辑的,那么关于表单的一套东西都适用,他要可以验证,可以重置,也可以联动。

4.2 API 设计

// table 配置,需求对应的模块对应了他的配置在整个配置中的位置,显然行内编辑是和列相关的
{columns: [ // HEAD/ROW 相关{   dataKey: 'cityName', // 展示时操作的变量editKey: 'cityValue', // 编辑时操作的变量customField: SelectField, // 编辑状态的类型config: {}, // 编辑状态的一些配置renderChildren: function() {return [{id: 'bj', name: '北京'},{id: 'hz', name: '杭州'}].map((item) => {return <Option key={item.id}>{item.name}</Option>});},rules: function(value) { // 校验相关return true;}}],onChange: function(result) {doSth(result); // result 包括 {data: 表格的所有数据, changedData: 变动行的数据, dataKey: xxx, editKey: xxx, pass: 正在编辑的域是否通过校验}}
}
// data 结构
{data: [{cityName: 'xxx',cityValue: 'yyy',name: 'xxx',__selected__: true, __mode__: "edit", // 用来区分当前行的状态}],currentPage: 1, // 当前页数totalCount: 50, // 总条数
}

4.3 代码设计

图:行内编辑模式下的表格架构

  • 所有的 CellField 继承一个基类 Field,这个基类提供通用的与 Table 通信,校验的方式,具体的 Field 只负责交互部分的实现。

  • 下面是这部分设计的具体代码实现,碍于篇幅,不在文章中直接贴出。

  • https://github.com/uxcore/uxcore-table/blob/master/src/Cell/CellField.js

  • https://github.com/uxcore/uxcore-table/blob/master/src/Cell/SelectField.js

5. 总结

这篇文章以复杂表格组件的开发为切入点,讨论了以下内容:

  • 组件设计的通用流程

  • 组件分层架构与 API 的对应设计

  • 组件设计中易用性与通用性的权衡

  • State 和 Props 的正确使用

  • 生命周期的实战应用

  • 父子级间组件通信

碍于整体篇幅,有一些和这个组件相关的点未详细讨论,我们会在本系列的后续文章中详细说明。

  • 数据的 不可变性(immutability)

  • shouldComponentUpdate 和 pure render

  • 树形表格 和 数据的递归处理

  • 在目前架构上进行折叠面板的扩展

从零开始的 React 组件开发之路 (一):表格篇相关推荐

  1. react复习总结(1)--react组件开发基础

    这次是年后第一次发文章,也有很长一段时间没有写文章了.准备继续写.总结是必须的. 最近一直在业余时间学习和复习前端相关知识点,在一个公司呆久了,使用的技术不更新,未来真的没有什么前景,特别是我们这种以 ...

  2. 【天赢金创】我是如何看待React 组件开发

    从 auto-ellipsis 看 React 组件开发 auto-ellipsis 是一个用于解决文本超长溢出截断并加 ... 的 React 组件. 关于 React 随着 React 的火热,随 ...

  3. vue结合饿了么_饿了么基于Vue2.0的通用组件开发之路(分享会记录)

    Element:一套通用组件库的开发之路 Element 是由饿了么UED设计.饿了么大前端开发的一套基于 Vue 2.0 的桌面端组件库.今天我们要分享的就是开发 Element 的一些心得. 官网 ...

  4. Vite + React 组件开发实践

    简介: 毫不夸张的说,Vite 给前端带来的绝对是一次革命性的变化.或者也可以说是 Vite 背后整合的 esbuild . Browser es modules.HMR.Pre-Bundling 等 ...

  5. React组件开发流程——利用React构建简单的可检索产品数据表

    tip:有问题或者需要大厂内推的+我脉脉哦:丛培森 ٩( 'ω' )و [本文源址:http://blog.csdn.net/q1056843325/article/details/54755521 ...

  6. 从零开始vim搭建Java开发环境之coc.nvim 篇

    前言 vim之美妙我就不过多介绍了,懂的自然懂.之前我已经有一篇文章介绍如何使用SpaceVim来搭建Java开发环境. 传送门:<从零开始vim搭建Java开发环境[视频]> 最近使用c ...

  7. 用 SOLID 原则保驾 React 组件开发

    概述 本世纪初,美国计算机专家和作者 Robert Cecil Martin 针对 OOP 编程,提出了可以很好配合的五个独立模式:后由重构等领域的专家 Michael Feathers 根据其首字母 ...

  8. React Native开发之路(三)

    一个APP开发,网络请求是必不可少的,在项目中我被分配到任务是网络请求参数的封装,所以本文主讲的是网络请求的封装. 先上代码,我再解释用法: import React, { Component } f ...

  9. React组件开发-仿哔哩哔哩移动端首页

    前言 什么是React? React是一个简单的javascript UI库,用于构建高效.快速的用户界面.它是一个轻量级库,因此很受欢迎.它遵循组件_设计模式.声明式编程范式_ 和 函数式编程 概念 ...

最新文章

  1. CPQuery, 解决拼接SQL的新方法
  2. MySQL实战面试题_Mysql实战面试题
  3. (转)青春有限,走红有年
  4. boost::spirit::karma::detail::format_manip相关的测试程序
  5. boost::spirit模块实现一个以逗号分隔的数字列表的生成器的测试程序
  6. 配置DNS辅助服务器:DNS系列之四
  7. RMI强制Full GC每小时运行一次
  8. Python实现自动推本地github博客到远程仓库
  9. 弱电工程集成商_弱电工程楼宇自控系统基础知识培训资料
  10. SLAM GMapping(4)SLAM处理器
  11. Java集合框架源码解读(4)——WeakHashMap
  12. Hive高级查询(group by、 order by、 join等)
  13. HDU 2079 选课时间(题目已修改,注意读题)(母函数)
  14. 如何通过TXT文件批量生成汉信码
  15. epub to mobi转换器
  16. STC89C52驱动W25Q32测试笔记
  17. 上周,维(系数字世界真)谛技术开了个大会
  18. 聚焦低代码SaaS云服务,让企业轻松办公!
  19. 全球与中国便帽市场深度研究分析报告
  20. Linux查看网络连接情况ss,Linux网络状态工具netstat和ss使用详解

热门文章

  1. SQL中的CASE WHEN语句
  2. 学习动态性能表(10)--v$session_longops
  3. DataTable的AcceptChange方法为什么不能在Update之前?
  4. Json.net|NH|Log4net|Test等工具下载地址
  5. 程序员 :超越软件蓝领的七种武器
  6. OpenCV图像分割Grabcut算法
  7. 机器学习入门学习笔记:(2.1)线性回归理论推导
  8. 数字图像处理实验(10):PROJECT 05-01 [Multiple Uses],Noise Generators
  9. 四位达林顿_ULN2069B - 80 V - 1.5 A四路达林顿开关 - STMicroelectronics
  10. php拆分jsion_PHP拆分YAPI导出的swagjson文件