React 导读(六)
阅读完之前的第四和第五章,分享了 Dialog
和 Table
组件的一点设计,还有一些小组件的代码都已经上传到 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: 系列写了六集 =.= 还蛮神奇的,我姓协音就是六。
文章来源:
Author:小撸
link:http://www.60sky.com/post/react-intro-6.html