04月09, 2018

React 导读(六)

阅读完之前的第四和第五章,分享了 DialogTable 组件的一点设计,还有一些小组件的代码都已经上传到 Github-smarty 上去了,能够自己翻阅看一下,接着我们开始整合这些组件来合成一个业务模块。

一、理解容器组件、展示组件

在现在流行的方案中,Redux 的出现迎来了容器组件、展示组件等概念的流行,其实在这之前 Flux 的方案已经有了这类划分,Redux 应该是让它更知名了。

容器组件(containers/employeeManage/index.jsx):又称充血组件,主要用于一个顶层组件的数据获取,以及一些副作用数据、业务逻辑的处理,我这里的状态订阅代码都放在此处。

展示组件(components/employee/**/*.jsx):又可以称贫血组件,跟容器组件不同的是它几乎没有业务逻辑,只是通过传递的 props 进行渲染,更像一个纯函数,(props) => component

这里多联想一下,如果把这里的组件类型,脱离 View 层的限制去连接后端的分层设计,其实他们也是有充血模型和贫血模型的,但是唯一不同的是,后端的分层只是对数据的操作,可以简单分为:

充血模型:(Domain + Bussiness Logic) => End

贫血模型:(Bussiness Logic + Service) => (Domain Model) => End

上面主要是领域模型(Domain)以及业务逻辑(Bussiness Logic)的关系,区别的话简单可以理解成业务逻辑存在的位置,这里就跟前端的组件划分是一样的,如何更好的划分[业务逻辑]就是设计和开发组件的重点。

二、容器组件代码

容器就有一点打包的趣味,容器+组件,就是打包一些组件在一起,那么我们需要打包哪些东西呢? 我们需要打包业务模块的所有功能的入口组件,比如这里我的功能有:

  • 模块名称
  • 搜索、按钮等操作栏
  • 人员表格
  • 删除、添加、编辑弹框

入口代码就这样子:

<div className="mod">
    <ModTitle>员工管理</ModTitle>
    <EmployeeHeader onAdd={this.handleAdd} />
    <EmployeeTable loading={this.state.isLoadingData}
        data={this.state.list}
        onEdit={this.handleEdit}
        onDelete={this.handleDelete} />

    {/* 各种弹框 */}
    <EmployeeDeleteDialog visible={this.state.visibleDeleteDialog}
        data={this.state.currentSelected}
        onClose={this.handleCloseDeleteDialog}/>
    <EmployeeAddDialog visible={this.state.visibleAddDialog}
        onClose={this.handleCloseAddDialog} />
    <EmployeeEditDialog visible={this.state.visibleEditDialog}
        data={this.state.currentSelected}
        onClose={this.handleCloseEditDialog} />
</div>

从上面的代码可以看出,我这里的容器组件 render 的内容都是一些包装了的自定义组件,有很多 props 的传递。那么这些 props 的值在容器组件中是如何获取的呢?那就要用到之前我们解释数据流程的内容了,这里温习一下主要代码:

// 1. 先看一下我们容器组件依赖的状态 state
const state = {
    list: [],
    currentSelected: null,
    visibleDeleteDialog: false,
    visibleAddDialog: false,
    visibleEditDialog: false,
    isLoadingData: false,
};

// 2. 这里的 state 我是都放在 Store 中进行托管的,所以我们在容器组件构造器里面需要获取初始值
constructor() {
    super();
    // Store 有一个获取状态的 getter
    this.state = EmployeeStore.getState();
}

这样我们的数据就初始化好了,接下来就是监听来更改状态,然后 props 里面的值就有了更新。

componentDidMount() {
    EmployeeStore.on("loadingData", this.handleLoadingData);
    EmployeeStore.on("updateList", this.handleUpdateList);

    // 第一次先请求一次员工列表
    EmployeeStore.getList();
}

// 只要 emit("updateList", newData) 就会触发 React 的更新方法 setState 这样整个组件数据就重新更新了
handleUpdateList(list) {
    this.setState(prevState => {
        return {
            list: list,
            isLoadingData: false,
        };
    });
}

三、弹框

其实容器组件要做的工作我这里就差不多了,因为功能很简单。那么我这里只是处理了列表更新,对于列表数据的删除和添加等功能都没有做,这些副作用其实我是放在了弹框里面去做,无论你放在哪里都是可以的,看你项目的情况。我们这里只看一下添加弹框的内容,添加弹框是一个业务类型的组件,依赖的是 Dialog 基础组件的功能。

// addDialog/index.jsx
render() {
    if(!this.props.visible) {
        return null;
    }

    const footer = (
        <DialogFooter
            onSubmit={this.handleSubmit}
            onClose={this.handleClose}
            submitText="添加"
            closeText="关闭" />
    );

    return (
        <Dialog className="employeeAddDialog"
            renderHeader={() => <DialogHeader title="添加成员" />}
            renderFooter={() => footer}>
            <div className="addDialog">
                <Input onChange={this.handleChangeName} placeholder="请输入姓名" />
                <Input onChange={this.handleChangeDays} placeholder="请输入天数" />
                <Input onChange={this.handleChangeAge} placeholder="请输入年龄" />
            </div>
        </Dialog>
    );
}

上面的代码可以看出来,其实主要就是多了三个输入框来进行交互,最后这个添加弹框是有自己的状态的,不是一个纯展示组件,他的 state 如下:

state = {
    formData: {
        name: "",
        age: 0,
        sex: null,
        days: 0,
    }
};

这里我喜欢包裹一层 formData,用来单独就值我要跟后端对接的数据,可能需求还有变化,需要添加的数据并不想影响我的表单提交数据,而且更新也相对方便一点。

那么弹框的提交和关闭是怎么弄的呢?

handleSubmit(e) {
    const {formData} = this.state;
    // addEmployee 方法会 emit("updateList", newData) 来更新容器里面的状态
    EmployeeStore.addEmployee(formData);
    // 乐观交互
    this.handleClose();
}

handleClose(e) {
    // 把关闭状态给外面控制
    this.props.onClose();
}

这里可以看到关闭是通过 props 传递的一个容器方法来做的,因为弹框总体上来说,都是不具备重业务逻辑的一个组件,所以渲染的控制对外开放会更好,因为有时候万一你弹框是默认就要显示的就很别扭:this.state.visible = this.props.visible 这种代码就很蛋疼。所以干脆控制都让外面来做,要改变的给一个 onChange 的接口就行。

这里需要注意的就是添加弹框继承的是 React.PureComponent 来减少不必要的渲染,会进行一个浅的数据 Diff 控制更新。

细心的朋友可能会发现,我的 Input 组件写得比较恶心,为什么呢?我绑定了三个不同的 handleChangeXXX 这种方法,那么如何优化呢?可以思考一下哦~

四、展示表格组件

在这里我将表格做成了纯粹靠外界数据进行渲染的展示组件,将业务逻辑和行为几乎都给了容器组件去做,先来看一下目录结构:

├── dataTable
│   ├── config.js // 表格的配置,主要是 columns、格式化方法
│   ├── dataTable.css
│   └── index.jsx // 表格组件

根据之前的需求,表格这里具有:表头内容 两部分,这里表头能够根据我们的配置进行初始化:

// 具体的配置内容能够在 Github 上看一下
import config from "./config";

const headers = config.table.map((row, index) => {
    let width = row.width;
    return <th onClick={row.onSort} style={row.style} className={row.className} width={width} key={`header-${index}`}>{row.title}</th>;
});
headers.push(<th width="160" key="opts">操作</th>);

表头初始化好了,开始初始化内容,表格的内容这里按行进行初始化,依赖了一个 SimpleRow 的业务组件来减少业务表格的代码量:

const rows = this.props.data.map((item, i) => {
    return (
        <SimpleRow key={item.id} tableConfig={config.table} row={item} index={i}>
            <td key={`opt-${i}`}>
                <Button style={{marginRight: 5}} color="blue"
                    onClick={() => this.props.onEdit(item)}>编辑</Button>
                <Button color="red" onClick={() => this.props.onDelete(item)}>删除</Button>
            </td>
        </SimpleRow>
    );
});

这样我们的一个表格就搞定了~加入 Table 组件就能够渲染一个简单职工列表了:

<div className="mod-table employeeTable">
    <TableLoading loading={loading} />
    <Table>
        <Table.Header>
            {headers}
        </Table.Header>
        <Table.Body>
            {rows}
        </Table.Body>
    </Table>
</div>

这里可以看到还有一个 TableLoading 组件,这也是一个偏业务的组件,主要就是用于表格加载数据时候的一个 Loading 行为。最后这个表格是搞定了,但是代码还是复杂了一点,那么怎么才能更简单的让别人用呢?让代码更少,O__O "…这个可以再思考一下哦~可以再进行一次封装。

PS: 有什么东西是分层搞不定的...如果搞不定,再分一层。哈哈,当然这是一个段子。

今天就写到这里吧,其实导读系列就先结束了,这里涉及的东西不多,但是对最初用 React 来编写代码还算比较有帮助的,后面会继续深入一点的话题,比如

  • 库类:热火朝天的数据状态管理、纯函数、流式、单向数据流、表单、验证器等等吧~
  • 脚手架:如何搭建一个自己心仪的玩具~
  • 框架:如何更工程的去开发 React 等内容~
  • 好玩的一些开发组件的理念~
  • 当 TypeScript 遇上 React 呀这种。

想到的时候有时间就写写。

PS: 系列写了六集 =.= 还蛮神奇的,我姓协音就是六。

本文链接:http://www.60sky.com/post/react-intro-6.html

-- EOF --

Comments

Hello World!