我的React最佳实践

一些问题


react为前端开发带来了很多的便利,配合一些前端组件库,我们能够十分迅速的开发出一个完整的前端项目。但是在使用react开发的过程中同样会遇到很多问题,在个人的react开发过程中,认为主要集中在这几个部分:

  1. 组件状态管理
  2. 组件代码和业务代码强耦合
  3. 组件的粒度
组件状态管理

组件状态管理主要包括两个部分:一个是组件的状态存在哪里,另一个是组件的状态该如何存储。
对于第一个问题,其实比较好解答,对于一个复杂的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进行操作。在数据层中我们依赖了三个库,分别是reduxredux-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操作。
文章还有很多不足的地方,望斧正,共勉~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容