从零开始的 React 组件开发之路 (一):表格篇
React 下的表格狂想曲
0. 前言
欢迎大家阅读「从零开始的 React 组件开发之路」系列第一篇,表格篇。本系列的特色是从 需求分析、API 设计和代码设计 三个递进的过程中,由简到繁地开发一个 React 组件,并在讲解过程中穿插一些 React 组件开发的技巧和心得。
为什么从表格开始呢?在企业系统中,表格是最常见但功能需求最丰富的组件之一,同时也是基于 React 数据驱动的思想受益最多的组件之一,十分具有代表性。这篇文章也是近期南京谷歌开发者大会前端专场的分享总结。UXCore table 组件 Demo 也可以和本文行文思路相契合,可做参考。
基本思路是通过遍历列配置来生成每一行
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 {${rowData.city} - ${rowData.name}
}
}
}
- 提供定制化渲染的两种方式:
渲染函数 (更推荐)
{
render: function(rowData) {
return
},
}
- 渲染组件
{
renderComp: , // 内部接收 rowData 作为参数
}
- 推荐渲染函数的原因:
函数在做属性比较时,更简单
- 约定更少,渲染组件的方式需要配合 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 的生命周期
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 和 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 代码设计
图:行内编辑模式下的表格架构
所有的 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 PC 组件库 UXCore ,上面提到的点,在我们的组件开发工具中都有体现,欢迎大家一起讨论,也欢迎在我们的 SegmentFault 专题下进行提问讨论。
关键字:JavaScript, react.js, 前端, 组件化
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!