2018年3月19日星期一

React 导读(五)

React 导读(四)中分享了组件设计最开始考虑的一些事情,不能介绍太矛盾了,其实对于设计来讲是有正反两面的分析的,就跟评论历史事件来看都是要分两面进行分析的。今天我们接着分享剩下的内容,我觉得不一定要求多,但是一定要带着思考来写,有点意识流。

首先弹框和表格是最常用的组件,下面就介绍表格吧。

表格其实是很复杂的一个组件,我们先来看下成熟的表格,然后再来看下我实现的最简单的表格组件是什么样子的。这里就看下比较流行的 antd 吧,我不会分析他的实现和代码,因为这并不利于第一次学习一个组件的封装,一个成熟的组件一般是迭代出来的,越是迭代时间长的组件就更复杂,学习前应该是知道有一个东西,然后将这个东西简化,知道它是什么,然后进行扩展会比较好一点。

这里我将表格组件的实现分为 2 类:

  1. 配置式
  2. 组合式

2种没有哪一种好,各有优缺点:

配置式优点

  • 易于使用
  • 功能声明清晰,具有一定的整体性,代码也不分散

配置式缺点

  • 扩展、定制性其实是相对组合式较差
  • 递归性低于组合式
  • 组件结构非标记语言表达,在第一层阅读代码的时候主观上认为不够清晰

组合式优点

  • 组件结构标记语言实现,更清晰
  • 扩展性较强,不容易在代码里面形成较多的 if-else 式编程

组合式缺点

  • 使用上相比配置式复杂
  • 代码量较多

这里分的比较开,但是可以是配置+组合的方式来实现组件,做一个制衡。比如:

  • 表头无论是动态、静态构造,最后都是一个固定的配置式数据,也许你还能够自定义列展示;
  • 数据的结构 ID 与 Columns key 进行统一的配置,其实从某种意义上数据与列也是一种约定的模式;
  • 分页组件比较固定,就需要固定配置等等。

对于 antd-table 是依赖于 rc-table 组件进行封装的,props 主要有 2 个:columns、dataSource。

配置上的基础结构,我这里用自己的描述代码:

// 一个 JSON 的结构
ColumnItem {
    // String 表示 title 的数据类型是字符串
    title: String;
    key: String;
    dataIndex: String;
    // 表示 render 是一个非必填的配置,是一个函数,有 2 个值,可以返回一个 JSX 结构内容
    render?: (text, record) => JSX.Element;
}

Columns = ColumnItem[]

然后使用的时候通过把配置传递进去就返回了一个你配置的表格:

<Table columns={columns} dataSource={data} />

当然 antd 的体验是不错的,还支持将 Column 组件当作子组件的方式声明,也就是我说的配置+组合的一种结合,会将可读性增强、扩展外置的优点放大。具体可以参考官方文档,这里不能介绍太多:antd-table-cn-document

随着需求的扩展,<Table /> 组件需求逐渐增多,就会出现更多的依赖 props,也许会做更多的判断。虽然从需求上我们不能控制这种 if-else 的逻辑,但是我们能够通过平行组合的方式来减少单个组件的复杂度。这个是双刃剑,因为可能会有冗余。

平行组合的方式就好比有很多 if 的条件,根据组件的种类进行拆分,分成不同的组件,然后让使用者去决定用什么组件,这样一层一层的铺展使用,重的表格可能会有聚合的组件模式,这个又是另外一个问题了。

那么我这里例子里面用到的表格组件怎么弄的呢?我模仿的是 bootstrap-table 的配置方式,然后再配合 React 的一些组件特性拼起来实现的一个 Table 组件。

我这边实现的表格组件只有以下四个组件,其中 SimpleRow 组件是一个 Row 最基础的用法,满足基本的一个表格行。Table.HeaderTable.Body 分别是对表头和表内容的装饰,主要是样式和布局上的统一。

Table
    Table.Header
    Table.Body
    SimpleRow

具体的 Table.jsx 组件代码如下,基本等于就是一个壳:

export default class Table extends React.Component {
    render() {
        const {children, className} = this.props;
        const classNames = cx('st-table', className);

        return (
            <table className={classNames}>
                {/* 这里就意味着这个组件是层级 Wrap 类型的组件 */}
                {children}
            </table>
        );
    }
};

我们先看一下会如何使用表格吧。

<Table>
    <Table.Header>
        <th>...</th>
    </Table.Header>
    <Table.Body>
        <tr>...</tr>
    </Table.Body>
</Table>

React 组件是能够有命名空间的写法的,比如这里的 Table.<Someone> 的写法。那么我们要统一样式和布局,基本是等于就统一一下 class 就行,我们不能说让使用者自己每次去添加一些共有的代码,所以这里我们要用一个 API React.cloneElementReact.Children.map,后面 map 方法可能比较好理解,类似 Array.prototype.map,那么 React.cloneElement 的意思就跟它的表面意思一样:克隆一个元素(ReactElement),API 可以先看下官网 cloneElementApimapApi

先看看一下 Table.Body 的代码吧:

Table.Body = ({ children }) => {
    // 1. 先遍历孩子元素
    const cloneChild = React.Children.map(children, (child, i) => {
        if(!child) {
            return null;
        }
        
        // 2. 要更新的孩子组件的属性
        const wrapProps = {
            key: `st-table-row_${i}`,
            className: cx('st-table-row', child.props.className)
        };

        // 3. 返回一个新的 clone 的 React 元素,这里可以思考一下为什么要 clone
        return React.cloneElement(child, wrapProps);
    });
    
    // 4. 最后返回一个 tbody 原始的容器
    return (<tbody className="st-table-tbody">{cloneChild}</tbody>);
};

这里的代码可以看 github 上完整的源码:TableComponent

那么我们组件的结构通过组合的写法构造好了,那么我们的 Columns 在哪儿去声明呢?我们这里也是通过 js 数组对象的方式来声明,结构如下:

const table: [
    {
        field: 'id',
        title: 'ID',
        width: 50
    },
    {
        field: 'sex',
        title: '性别',
        format(value, row, index) {
            if(!value) {
                return '保密';
            }
            
            if(value === Gender.Girl) {
                return '女';
            } else if(value === Gender.Boy) {
                return '男';
            }
        }
    },
    ...
];

这里的配置怎么和表格的列连接起来呢?我这里将业务表格组件的目录划分成了这样:

├── dataTable
│   ├── config.js // 表格的配置
│   ├── dataTable.css // 表格的样式
│   └── index.jsx // 表格组件
// 初始化 header
const headers = table.map((row, index) => {
    let width = row.width;
    return <th key={`header-${index}`} width={width}>{row.title}</th>;
});
// 多一个操作列,这个操作列我这里是没有 antd 那么智能,识别到差 1 列的时候就成为了操作什么的
headers.push(<th width="160" key="opts">操作</th>);

// 初始化每行的内容
const rows = (this.props.data || []).map((item, i) => {
    return (
        <SimpleRow key={item.id} tableConfig={table} row={item} index={i}>
            {/* SimpleRow 通过传入的 TableConfig 和 Row 数据,初始化好了基本的行元素 */}
            {/* SimpleRow 也是一个 this.props.children 的结构 */}
            <td key={`opt-${i}`}>
                <Button color="blue" onClick={() => this.props.onEdit(item)}>编辑</Button>
                <Button color="red" onClick={() => this.props.onDelete(item)}>删除</Button>
            </td>
        </SimpleRow>
    );
});

这样回到最开始那个使用表格的例子,将 header, rows 都添加进入即可,最后整个表格 Row 的效果就是这样子,SimpleRow 的实现代码依然能在业务表格 Github 文件中找到:

这种写组件的方法特别费代码,所以是否可以考虑封装成一个业务通用的表格组件呢?这肯定是可以的,将变化的部分通过 props 传入即可,这个工作可以自己动手试试,在我的源码基础上进行修改。还有就是 tableConfig 里边的对象属性,你是能按自己需求进行添加的,可以在看看源码中的配置。

最后表格实现的效果,虽然没有排序、固定表头,但是用来展示信息是已经完善了,比如点击表头进行排序这个可以动手试试,在我组件的基础上应该比较好添加:


这里表格组件就实现了,你肯定会想,为什么没有 antd 那么方便实用和强大呢?

(1) antd-table 和这里介绍的 Table 组件层级是不同的,更接近的应该是 rc-table;
(2) 这只是针对最简单的表格使用场景进行设计的,按的是自由编写修改,并不是一个复杂场景的表格设计。

今天先写到这里吧~11点半了,休息一会儿。

没有评论:

发表评论