一些问题
react为前端开发带来了很多的便利,配合一些前端组件库,我们能够十分迅速的开发出一个完整的前端项目。但是在使用react开发的过程中同样会遇到很多问题,在个人的react开发过程中,认为主要集中在这几个部分:
- 组件状态管理
- 组件代码和业务代码强耦合
- 组件的粒度
组件状态管理
组件状态管理主要包括两个部分:一个是组件的状态存在哪里,另一个是组件的状态该如何存储。
对于第一个问题,其实比较好解答,对于一个复杂的react前端项目,依赖于类似redux的flux流,将组件状态存储在一个全局的store中是一个很好的选择。在复杂前端项目的开发中,除了一些基础公共组件,我会尽可能的避免将组件状态存储在组件state中。主要原因有两点,一个是项目复杂后,组件间的状态传递会变得异常恶心,需要依靠大量props属性传递函数,并且强依赖组件生命周期函数。第二个从设计角度出发,react组件其实可以理解为一个纯函数,它根据传递进去的属性做相应的渲染,如果组件自己保持了state,那么它可能会导致意料外的渲染结果。
第二个问题比较麻烦,对于一个组件,它的状态可能包括两个部分,一种可能是data,它们通常来自于server,比如一个列表list。另一种则是state,它表示的是页面状态,比如一个table 的loading状态。这两种数据有时候甚至会有交集,因此如何管理它们也是一个问题。
组件代码和业务代码强耦合
react是一个负责view层的前端库,所以组件本身是不包含业务逻辑的。抽象来说,组件其实就是一个空的盒子,你给它什么,它就渲染出什么状态,因此具体的业务逻辑不应该在盒子里面做,而应该抽象成具体给这个盒子什么样的数据。因此在组件实现过程中,应该避免参杂各种业务逻辑代码,但这个其实不太好实现,对于非redux项目,你可能在代码中能够看到各种setState方法调用,而在redux项目中,则是各种dispatch。
组件的粒度
在一个项目中,我们会写很多组件,有时候为了考虑组件的复用性,我们会把一个复杂的组件拆分成很多个基础组件。基础组件的实现其实要比业务组件复杂很多,因为服用的问题需要考虑更多的分支条件和边界条件,对于开发速度会造成很大影响。
解决思路
其实对于上面的问题,并没有银弹,也就是很难找到一个包治百病的方法。更多的也是根据不同的业务场景做具体的实现,因此虽然这篇文章说的是最佳实践,其实也只是对应一些场景下的解决思路,但是应该能够解决大部分应用场景下的痛点。
项目的整体框架如下,其实相对于其它react前端项目来说,它最大的不同可能就是抽象出了一个业务层,下面我们具体介绍各个部分。
数据层
依赖于redux,我们将所有的状态存储在一个全局的store中,所有对于store的修改都是通过action进行操作。在数据层中我们依赖了三个库,分别是redux和redux-actions以及react-redux。redux应该是目前使用最广的flux库,依赖redux我们可以很好的管理store中存储的状态。而依赖redux-actions,我们能够快速的创建actions,并且在reducer中监听它们,react-redux可以通过connect方法将store中的数据映射到组件的props中。
在store的设计上,我将它分成了两部分,一块是用来存储来自server的数据data,另一部分则是组件本身的状态state。这样带来的好处是可以解决某些状态回退需求,比如我们有一个修改配置的操作,如果只有state存储配置数据,那么这次修改保存失败后,我们无法退回到修改前的状态。
配置的数据通常来自于远端,因此原始数据存储在data中,而这个弹窗表单的状态则存储在state中,这里存在一个问题,就是我们需要讲data中的数据拷贝一份到state中,同时保存成功后,也需要对data中的数据做一下更新。对于数据的保存,我们依赖immutable,关于immutable有许多文章介绍,这里就不做详细介绍。
#actions
import { createAction } from 'redux-actions';
export const setCount = createAction('SETCOUNT');
#reducer
import { handleActions } from 'redux-actions';
import Immutable from 'immutable';
export const test = handleActions({
SETCOUNT: (state, action) => state.setIn(['data', 'count'], action.payload),
}, Immutable.fromJS({data:{count: 0}}));
#View
import React from 'react';
import { connect } from 'react-redux';
const mapStateToProps = state => ({ count: state.getIn(['data', 'count'])});
const Test = (props) => {
return (<div>{props.count}</div>)
}
export default connect(mapStateToProps)(Test);
#这样调用dispatch(setCount(10))就可以完成修改count的操作
业务层
在过去的项目中,具体的业务逻辑通常在View层中完成,因此我们能够在view层的代码中看到许许多多的dispatch方法,异步fetch方法,以及数据组装方法。这种情况其实是讲业务逻辑和前端组件进行了强耦合,造成了组件的复用性变差,同时如果组件设计不合理,debug的时候问题的定位会变得异常困难,你往往分不清是组件出了问题,还是业务代码出了问题。
为了解决这个问题,在实践过程中我们单独的抽离了一层业务逻辑层,业务层中一个比较难处理的问题就是如何处理异步操作,这里我们主要依赖的是redux-thunk,这个组件其实很简单,代码也很少,感兴趣的可以看看。它的主要作用是可以让我们dispatch异步方法,同时依赖react-redux中的bindActionCreators方法将业务层中的方法绑定到组件的props中。
我们也将全局store也注入到了业务层代码中,通过store.getState()方法和依赖reselect获取状态树中的数据。而不依靠view组件中定义的方法传递的参数。
通过上面的操作,前端组件和业务逻辑进行了充分的解耦。
#actions
import { createAction } from 'redux-actions';
export const setCount = createAction('SETCOUNT');
export const setServerData = createAction('SETSERVERDATA');
#reducer
import { handleActions } from 'redux-actions';
import Immutable from 'immutable';
export const test = handleActions({
SETCOUNT: (state, action) =>
state.setIn(['data', 'count'], action.payload),
SETSERVERDATA: (state, action) =>
state.setIn(['data', 'serverCount'], action.payload),
}, Immutable.fromJS({data:{count: 0, serverCount: 0}}));
#业务层actionCreators
import {setCount} from 'actions';
import store from 'reducer/store';
import { getCount } from 'selectors';
export const addOne() {
const count = getCount(store.getState());
dispatch(setCount(count + 1));
}
export const fetchDataFromServer() {
fetch('url')
.then(res => {
dispatch(setServerData(res))
})
.catch(e => { // error handle})
}
#业务层selectors
import { createSelector } from 'reselect';
export const data = state => state.test.get('data');
export const getCount = createSelector(
data,
data => data.get('count')
)
#View 变得简单了
import React from 'react';
import { connect, } from 'react-redux';
import { bindActionCreators } from 'redux';
import { getCount } from 'selectors';
//将actionAreators中的方法绑定到view的props中
import * as actions from 'actionAreators'
const mapStateToProps = state => ({ count: getCount(state) });
const mapDispatchToProps =
dispatch => bindActionCreators({ ... actions }, dispatch);
const Test = (props) => {
addOne() {
this.props.addOne();
}
return (<div onClick={() => this.addOne}>{props.count}</div>)
}
export default connect(mapStateToProps,mapDispatchToProps)(Test);
#这样调用dispatch(setCount(10))就可以完成修改count的操作
View层
对于前端组件我们将它分为三种,一种是基础组件component,一种是业务组件widget,以及顶层组件View。
对于基础组件component,一般是粒度很细,跟业务没有太大关系的组件。它们一般是公司自己的前端组件库,或者一些开源组件库,以及项目中通用的组件。基础前端组件的开发相对来说比较困难,因为需要考虑很多边界条件和分支条件,同时还需要提供比较完善的接口,特别是针对于一些有输入属性的组件更加需要注意。
前端基础组件的设计因为需要考虑到组件的复用性,会在组件中保留state,以及使用setState方法,而不是像其他类型组件一样完全依赖于传入的属性。这里推荐一下蚂蚁的antd库;
业务组件widget一般是由多个基础组件组成的组件,它们一般不保存自己的state,同时也不像View组件一样从store中获取数据,而是完全通过传入的props来进行渲染,同时它的事件handle函数也完全依赖于顶层View组件传入。
View组件式顶层组件,它通过connect链接store中的数据和业务层中的方法,并将它们传入到子组件的props中,这样View组件就相当于整个页面的入口,因此当出现问题的时候,我们可以很好的定位,从View层一层层捋就行,而不用担心是不是某个widget组件出了问题。
#Widget
import React from 'react';
export const ChildTest = (props) => {
click() {
this.props.clickHandle();
}
return (<div onClick={() => this.click}>{props.count}</div>)
}
#View
import React from 'react';
import { connect, } from 'react-redux';
import { bindActionCreators } from 'redux';
import { getCount } from 'selectors';
//将actionAreators中的方法绑定到view的props中
import * as actions from 'actionAreators'
import ChildTest from './widgets/ChildTest'
const mapStateToProps = state => ({ count: getCount(state) });
const mapDispatchToProps =
dispatch => bindActionCreators({ ... actions }, dispatch);
const Test = (props) => {
addOne() {
this.props.addOne();
}
return (<div><ChildTest clickHandle={() => this.addOne} /></div>)
}
export default connect(mapStateToProps,mapDispatchToProps)(Test);
这种设计思路下可能会造成的问题就是可能组件结构会很深,不过这种情况可以在设计widget的时候解决,遇到很深的情况下我们最好将深层组件抽离出来,然后作为组件的props.children传入。
其它
整个项目的结构一般如下:
.
├── business
│ ├── actionCreators
│ └── selectors
├── common
│ ├── Constants.js
│ ├── Utils.js
│ └── apis
├── components
│ └── OptComponent.js
├── main.js
├── public
│ └── index.html
├── redux
│ ├── actions
│ ├── index.js
│ └── reducers
├── router.js
└── views
├── Index
└── Layout
存在的问题主要在于异步的处理,后面发现逻辑复杂后,redux-thunk有一些力不从心。可能能使用其它换方案,比如redux-saga。
写在最后
这个框架实践有一段时间了,大部分场景都能比较好的处理,但是对于一些复杂的场景也存在一些问题,比如大量的异步请求,异步竞态出现的场景,这套框架可能就不太适合。
在整个实现过程中,其实很多思想都是借鉴了后端的,其实业务层其实也就是后端的biz层。数据层毋庸置疑对应后端的数据层,只不过不是像mysql那样的关系型数据库,而是mongodb或者redis这样的非关系型数据库,而action则是对应的dao操作。
文章还有很多不足的地方,望斧正,共勉~~