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

React 下的表格狂想曲

0. 前言

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

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

超级产品经理

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

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

{
city: '北京',
name: '小李'
}

  1. columns 中的每一个元素是一列的配置,也是一个 hash 对象,至少应该包括如下几部分:

{
title: '表头',
dataKey: 'city', // 该列使用行中的哪个 key 进行显示
}

  1. 易用性与通用性的平衡

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

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

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

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

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

  5. 从易用性角度出发

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

  1. 从通用性角度出发

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

  1. 提供定制化渲染的两种方式:

渲染函数 (更推荐)

{
render: function(rowData) {
return
},
}

  1. 渲染组件

{
renderComp: , // 内部接收 rowData 作为参数
}

  1. 推荐渲染函数的原因:

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

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

1.3 代码设计

超级产品经理
{/ 非受控模式 /}

value 配置时,input 的值由 value 控制,value 没有配置时,input 的值由自己控制,如果把 看做一个组件,那么此时可以认为 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 的生命周期

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

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

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

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

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

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

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

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

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

父子级组件间的通信

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

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

图:父子级间的通信

3. 自行获取数据

3.1 需求分析

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

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

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

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

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

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

3.2 API 设计

// table 配置,需求对应的模块对应了他的配置在整个配置中的位置
{
url: "//fetchurl.com/data", // 数据源,只支持 json 和 jsonp
fetchParams: { // 额外的一些参数
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 {item.name}
});
},
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 代码设计

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

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

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

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

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

5. 总结

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

  1. 组件设计的通用流程

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

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

  4. State 和 Props 的正确使用

  5. 生命周期的实战应用

  6. 父子级间组件通信

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

  1. 数据的 不可变性(immutability)

  2. shouldComponentUpdate 和 pure render

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

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

最后

惯例地来宣传一下团队开源的 React PC 组件库 UXCore ,上面提到的点,在我们的组件开发工具中都有体现,欢迎大家一起讨论,也欢迎在我们的 SegmentFault 专题下进行提问讨论。

关键字:JavaScript, react.js, 前端, 组件化


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部