从零开始的 React 组件开发之路 (一):表格篇
React 下的表格狂想曲0. 前言欢迎大家阅读「从零开始的 React 组件开发之路」系列第一篇,表格篇。本系列的特色是从 需求分析、API 设计和代码设计 三个递进的过程中,由简到繁地开发一个 React 组件,并在讲解过程中穿插一些 React 组件开发的技巧和心得。 为什么从表格开始呢?在企业系统中,表格是最常见但功能需求最丰富的组件之一,同时也是基于 React 数据驱动的思想受益最多的组件之一,十分具有代表性。这篇文章也是近期南京谷歌开发者大会前端专场的分享总结。UXCore table 组件 Demo 也可以和本文行文思路相契合,可做参考。
1. 一个简单 React 表格的构造1.1 需求分析
1.2 API 设计
1.3 代码设计
2. 加入更多的内置功能
2.1 需求分析
2.2 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 代码设计结构图
内部数据的处理
何时该用 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 的值由自己控制,如果把 <input /> 看做一个组件,那么此时可以认为 input 此时有一个 state 是 value。显然,无 value 状态下的配置更少,降低了使用的成本,我们在做组件时也可以参考这种模式。 例如在我们希望为用户提供 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 对象传地址的特性,如果我们直接对比 生命周期的使用时机
父子级组件间的通信父级向子级通信不用多说,使用 prop 进行传递,那么子级向父级通信呢?有人会说,靠回调啊~ onChange等等,本质上是没有错误的,但当组件比较复杂,存在多级结构时,如果每一级都去处理他的子级的回调的话,不仅写起来非常麻烦,而且很多时候是没有意义的。 我们采取的办法是,只在顶级组件也就是 Table 这一层控制所有的 state,其他的各个子层都是完全由 prop 来控制,这样一来,我们只需要 Table 去操作数据,那么我们逐级向下传递一个属于 Table 的回调函数,完成所有子级都只向 Table 做“汇报”,进行跨级通信。
3. 自行获取数据3.1 需求分析作为一个尽可能为用户提高效率的组件,除了手动传入 data 外,我们也应该有自行获取数据的能力,用户只需要配置 url 和相应的参数就可以完成表格的配置,为此我们可能需要以下参数:
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 代码设计
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',__mode__: "edit",// 用来区分当前行的状态 }],// 总条数 } 4.3 代码设计
5. 总结
最后惯例地来宣传一下团队开源的 React PC 组件库 UXCore ,上面提到的点,在我们的组件开发工具中都有体现,欢迎大家一起讨论,也欢迎在我们的 SegmentFault 专题下进行提问讨论。
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |