[React-章节11 终结篇] 做一个留言板项目 之 重构至redux

思考

最近我也一直在研究redux到底是干什么的,经过零零散散的学习之后,我有这么几个不算成熟的认识分享给大家:

  1. redux和react没有依赖关系,redux出现是提出了一种规范和模型,可以这么来描述它:程序(可以理解为UI/展现组件)某处产生一个action(理解为一个消息,一个事件),通过dispatch(理解为一个邮递员,信使)的派发,交由处理者reducer(其实就是处理action)进行最终的计算,结果存储到store(就是一个对象,存的是程序的状态state)里。
  2. redux在全局只维护一个store存储对象,程序分很多组件,那么各个组件的state状态都存储到这1个store里,同时每个组件的reducer处理器也聚合到一起挂接到store上。
  3. react把组件的展示,数据的异步获取,state的变化全部实现在component里,随着代码膨胀愈加复杂,而redux有利于我们将数据的处理和展示进行进一步分层,优化项目结构。
  4. redux将所有组件的state存储在1个store中,很自然的有利于跨组件的state共享,我个人认为这是非常重要的一个特性。如果没有Redux,我们的react跨组件共享信息,难免通过深层次的callback传递与notify思路去实现,是很麻烦的事情。

学习方法

其实我按照自己的计划学习实践到第11篇博客,已经感受到纯react代码混合实现异步数据和组件特效会导致代码一团糟。如果redux能够不费多大功夫帮我把『数据和计算』与『组件展现』分离开来,我还是愿意花点时间来掌握的。

学习redux,首先是基础知识:至少知道redux的理念,基础API,同时对redux如何结合react,甚至结合react-router有一个朦朦胧胧的认识,我认为就足够了,这方面应该看一下这篇教程

不过,我看完这个教程也挺迷糊的,并不清楚怎么应用到我的项目里来,所以我接着看了这篇博客,它主要是介绍react和redux是如何结合的,更加贴合实践。其中connect是我们最需要了解的API,它极大的简化了react和redux结合的背后问题,使得我们可以很自然的在react框架下套用redux的理念。

当然看完之后,大概扫一下这个github里的例子会加深一点理解,这个例子里用到了redux-simple-router这个库,它是为react-router封装的redux库,因为我的项目也使用react-router(我相信绝大多人都要用),因此后续实践也会涉及到这个库的使用。

打开它最新的官方地址,阅读一下介绍会发现它是redux-simple-router的前身,现在叫做react-router-redux,并且里面的例子似曾相识。我们知道react-redux是为react封装了redux便于使用,而react-router-redux是封装了react-router相关的redux逻辑,因此我们最终会同时使用react,react-redux,react-router-redux,它们分别只解决自己关心的问题,需要组合使用才能完成项目。

下载源码

redux改造之MsgDetailPage

重构项目

安装依赖

  • npm install redux –save
    • 安装redux不必多说
  • npm install redux-thunk –save
    • 这是一个redux的中间件,能够支持我们dispatch一个function而不是action对象,后面会看到具体啥意思,没那么复杂
  • npm install react-redux –save
    • 基于react封装的redux,从而我们可以很方便的为组件注入action和state到组件props,其实就是屏蔽redux原生API的复杂性
  • npm install react-router-redux –save
    • 为react-router封装了一下相关的redux逻辑,因为react-router是最外层的组件(<Router>还记得吗),我们的组件都套在里面。因此如果如果我们的组件要用redux,那Router组件不实现redux相关逻辑我们又怎么用呢,所以顺理成章。

创建store

redux本来创建store也只需要传入reducer函数就足够了,之后做的事情无非是定义一下action对象,通过dispatch方法交给reducer进行处理,所以store的创建仅需要reducer函数。

而比较简单的是,正因为react-router本身需要为store提供reducer函数,因此我自己的组件MsgListPage,MsgDetailPage不需要立马进行改造。我先把react-router的reducer注入到store上,把这个全局的store对象建立出来,代码应该可以正常运行。

这是Router.es6修改后的代码,做了几个修改点:

  • 引入redux,使用了combineReducers实现reducer聚合,createStore实现store创建,applyMiddleware引入中间件。
  • 引入react-redux,使用了它为react封装的外层容器<Provider>。
  • 引入react-router-redux,使用了它为react-router提供的routerReducer以及用于增强react-router的history能力的syncHistoryWithStore,用于取代原生的hashHistory(这样router的location数据也就存储到了store中)。
  • 引入了redux-thunk中间件,因为我有异步dispatch action的需求。

经过简单的修改,现在我们已经为redux铺垫好了基础环境,并且对我们现有的程序不会造成任何影响。这样,接下来我就可以从最简单的组件MsgDetailPage入手,为其实现action和reducer,同时把组件需要的state和action以props的形式注入到组件内,最终把reducer添加到store中。

MsgDetailPage详情页

按道理说,只要涉及到setState调用的相关逻辑链,都应该用redux重构。

最终的效果应该是component仅访问props即可完成UI渲染,而所有业务逻辑和状态管理都应该挪到action和reducer中处理。

为了方便翻阅,先贴出修改前、后的MsgDetailPage.es6的完整代码:

修改前的代码

修改后的代码

为组件注入state与action

从代码对比看出,这里使用export default connect….取代了原先的export default class MsgDetailPage。

  • mapStateToPorps函数用于将redux store中的state注入到component对象的props中,所谓『注入』其实就是只需要我们返回一个映射关系即可:其中key是props里的名字,value是redux store中某个字段的值。
  • mapDispatchToProps里调用了bindActionCreators,顾名思义是将我们实现的若干『action生成方法』注入到component的props里。
  • connect方法将上述2个注入函数关联到组件MsgDetaiPage,最终导出一个经过修饰包装的组件。
  • ownProps用于访问react-router提供给我们的一些数据,例如:获取url query参数和router捕获的params。
  • 关注一下代码里的注释,我们完全有能力将redux store中任何属性注入到组件中来访问,这就提供了非常方便的跨组件数据共享的目的。

编写action

我将涉及state修改的业务逻辑全部抽取成独立的『action生成方法』,这里有4个:

  • initState:重置redux store中的state为初始状态
  • adjustLoadingHeight(height):用于调整Loading界面的高度
  • fetchDetail(msgId):用于ajax获取文章内容
  • adjustContentHeight(height):用于调整文章内容的最小高度

action完整代码如下:

其中initState,adjustLoadingHeight,adjustContentHeight是『同步action』,也就是直接return返回一个action对象。这样,框架会帮我们隐式的dispatch这个action,从而交给reducer进一步进行处理,生效。

而fetchDetail则不同,它基于之前的redux-thunk中间件,它支持返回一个function而不是action对象。这个函数应该接受至少一个dispatch参数(第二个可选参数是getState用于获取当前redux store里的值)。我们返回的函数会立即被调用,我们可以使用传入的dispatch发起『同步action』,也可以先执行异步的ajax请求等待回调后再dispatch发起action,而后者就是『异步action』的含义了。

编写reducer

每个组件的reducer都应该与其实现的action对应,这样才能完整的实现redux的流程。

因此,reducer中也有对应的4个action的处理函数实现,它们接收现有的redux state和action对象,经过处理返回新的redux state,框架将帮我们存储到redux store中全局存储。

我们为组件MsgDetailPage导出唯一的reducer函数入口,它判断action.type后分发到具体的4个处理函数,需要注意的几个点如下:

  • MsgDetalPageReduer函数的initState参数应该定义成组件初始化的state,也就是此前通过mapSteteToProps()函数定义的那些state。这个做法和在组件的constructor里定义this.state类似,只不过现在是这些state被存储到了redux store中。
  • 在组件对象分配前,这个reducer函数会被框架调用,通过日志可以看到收到了这些action:Object {type: “@@redux/INIT”}、Object {type: “@@redux/INIT”}、 Object {type: “@@router/LOCATION_CHANGE”, payload: Object},从而让我们有机会返回组件的初始化state,这个过程也是在default分支中生效的。
  • 每个组件的reducer函数需要注册到redux的store中,可以回头看Router.es6中createStore的实现:

    因此,某一个组件的action被dispatch到store时,是有可能通知到其他组件的reducer中,因此default分支也有这方面的用途(不做任何动作,返回当前state),同时也意味着各个组件定义的action.type不能重复。
  • redux的state不能直接原地修改,需要拷贝一个副本进行修改,这里使用的Object.assign非常适合这个用途。
  • initState作用很大,因为redux store是全局维护的,因此重复进入同一个组件的话会访问到此前遗留在redux store中的state,而MsgDetailPage希望每次进入都是崭新的样子,所以在MsgDetailPage的构造函数里发起了一个用于重置store的action。

定义action.type常量

为了确保action.type全局唯一,所以在common/consts.es6中定义所有action的type。

回头看看组件的变化

我们回到开始的组件完整代码,对比发现代码中已经没有state关键字了,所有的状态都从this.props(从redux注入的全局state)中获取,所有的处理方法(用redux注入的action)也都是从this.props获取并调用的。

可见,通过redux我们原先的组件变得非常简单,仅仅是从this.props获取一下属性,渲染出组件的样子就可以了。而异步网络请求这样的操作都挪到了action中,对状态的变更都挪到了reducer当中,差别仅仅是现在的状态是redux store全局状态,而组件通过注入props的方法取代了直接访问this.state,仅此而已。

补充一个细节,在组件构造函数里action改动redux store,对应的state不会立即被注入回this.props而是在首次render完成后(componentDidUpdate调用后)立即进入一轮组件update,此时(componentWillReceiveProps调用)this.props才能读到你在构造函数里曾经变更的state。

通过本篇博客,对redux如何使用以及原理应该基本掌握,作为一系列react教程来说,至此也该画上句号。

后续,我可能会继续补充前端开发的相关基础知识,比如scss之类的,也可能会去动手实现一些比较常见的特效,比如滚动公告栏等。

后话

我后续已经把MsgListPage也重构成redux版本,现在你可以下载最新的完整代码进行运行体验了。

%e4%b8%8b%e8%bd%bd

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~