一开始我接触到redux是很懵的,看官方文档也很枯燥乏味,不理解说的什么意思,也是看过就忘,所以就结合公司的项目来熟悉一下redux。也可能有写的不对的地方,欢迎指出。
一、构建reducer
首先看storeConfig下的reducers.js。
combineReducers函数的作用是把一个或多个的reducer结合成一个reducer,作为createStore函数的第一个参数。
使用方法是:
combineReducers({ key1 : reducer1, key2 : reducer2 }) |
最终这个函数的返回的state的结构就是 {key1,key2},这个key值是可以自定义的,如果懒得再次命名,就可以使用ES6的简洁语法 combineReducer(reducer1, reducer2);返回的state就是 {reducer1, reducer2}的形式。
由于项目很庞大,在一开始就初始化所有的store是很繁重的,这里用到了动态加载redux模块的思想,把各个模块的redux分块存储,在项目首次加载时只加载一部分reducer(如layout 登录状态、全局配置,home首页的数据)。
上图的第三个参数asyncReducer代表的就是接收异步加载的模块的reducer,注入到根reducer中,这种异步按需加载reducer的方式也称作redux的动态注入。
如何加载异步模块,就是通过路由了,具体是如何实现的呢,我们来看路由的总文件routes\index.js,这个文件包含了所有模块的页面。
以申请模块为例,我们看看申请模块下的路由总文件\routes\ApproveV2\index.js,它里面包含了申请模块下所有路由的页面,是对整个模块的路由的进一步分发,我们需要再进一步看。
来看申请列表页\routes\ApproveV2\ApplyEntry.js,这个页就是如何把引入相应的reducer并传给根reducer。
- 首先引入redux配置中的injectReducer
- 定义页面路由
- 通过getComponent+webpack ensure实现在路由跳转时,异步加载该模块
- 引入该模块相应的reducer
- 通过injectReducer函数,传入两个参数。
- 第一个参数为store,因为异步获取的redux模块的reducer都保存在store.asyncReducer中,最后在makeRootReducer函数中通过扩展运算符给了combineReducers。
- 第二个参数为一个对象,键有key和reducer,还记得我们之前说的向combineReducers函数传递reducer函数时可以自定义reducer的名字吗,key就是指定的申请列表的名字为applyEntry,这样state中就有了state.applyEntry,并传入引入的模块的reducer
- 加载该组件,进行路由的跳转。
那么在初始化组件之前,数据就需要从之前的 变为这样
把异步获取的reducer和原来的reducer结合在一起,是如何实现的呢?继续看这个文件storeConfig\reducers.js
它把接收来的store.asyncReducers传入一开始的makeRootReducer函数,通过replaceReducer(nextReducer)函数来立即加载替换原来的reducer,从而改变state为新的数据结构,调用combineReducer函数,生成新的根reducer。
打印store,我们可以看到在申请的injectReducer之前只有home
在申请的injectReducer之后就加上了申请的reducer
二、创建Redux store
接下来就来看 storeConfig\createStore.js文件,
创建store的函数就是createStore,
用法如下:
createStore(reducer,[preloadedState],enhancer) |
第一个参数是是reducer,传入我们上面介绍过的reducers.js文件提供的makeRootState。
第二个参数是初始的state,传入的是一个空对象,
第三个参数是一个组合 store creator 的高阶函数,返回一个新的强化过的 store creator,可能不知道增强器是什么,那我们先来看他传入的是个什么东西,
compose(...functions) 把传入的函数参数从右向左组合成一个最终函数,右边的函数执行完就作为一个参数传递给左边的函数。 |
我们来看一下这个函数怎么实现的,
可以看到函数内部使用的是reduce函数,它是这样说的:
reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。 reduce() 可以作为一个高阶函数,用于函数的 compose。 |
这也就验证了enhancer是一个高阶函数
举个例子:
给compose传入四个参数 compose(func1,func2,func3,func4) 就会执行核心代码 funcs.reduce((a, b) => (...args) => a(b(...args))) 也就是 [func1,func2,func3,func4].reduce((a, b) => (...args) => a(b(...args))) reduce函数从左向右执行,a参数代表上一次执行的返回值,b参数代表数组当前遍历到的值,b参数就作为下一次执行的参数传入。 这样最终的结果就是,最右边的函数可以传入任意参数,在外部调用的时候传入,其他函数都只能接收一个参数,就是上一次函数返回的结果。 func1(func12(func3(func4(...args)))) 也就验证了compose的定义:把传入的函数参数从右向左组合成一个最终函数 |
下面我们来具体传入的是什么函数:
相关代码如下:
可以看到用到了applyMiddleware,redux-thunk,redux-logger。
先来看传入applyMiddleware函数的middleware参数是redux-thunk,它是一个现成的“中间件”库,我们先不管这个库具体怎么实现的,先了解一下中间件的概念,它可以提供位于 action 被发起之后,到达 reducer 之前的扩展点,就是如果我们不想让dispatch action之后,就立马更新state,而是在执行过程中可以做一些操作,如打印日志,异步发送ajax请求等。它的作用就是改造dispatch,在dispatch发出一个action之前,和action到达reducer之间添加一些东西。ps:具体怎么实现的,我实在是理解有限,还没搞清楚,想了解的再仔细看看官方文档。
如下面这个最简单的例子:
function dispatchAndLog(store, action) { |
引入redux-thunk库的作用就是用来解决异步问题的,单纯的redux,在dispatch action发出之后,就立即计算更新state,这是同步,而在我们的项目中,往往有用户的操作需要请求接口来更新state,这时候就需要异步来执行action,原本的store.dispatch只能接收action对象,但是在经过redux-thunk的处理之后,store.dispatch可以接收一个函数作为参数,并且这个函数带有dispatch和getState参数
如项目中的例子,可以看到能接收一个函数fetchIndexInfo,而这个函数就是action creater,用来请求接口、获取数据的
在store.dispatch执行完传入的函数中的axios请求之后,还会返回一个dispatch,用来继续执行redux事件, // 即把获取的数据存入store中 dispatch(updateIndexInfo(result.data)); export const updateIndexInfo = (value) => ({ |
以上就实现了异步更新state,这是我们系统最核心的地方。
而redux-logger就是一个可以打印日志的中间件,在我们执行dispatch时,他会打印出dispatch具体的执行过程,并且注意要放在所有中间件的最后,防止先执行了,无法打印出日志的情况。
可以看到在控制台打印如下:
在构造好所有的中间件之后,需要通过applyMiddlerware函数来把他们组合成一个数组,从右向左顺序执行。实现的原理也是我们上面介绍过的compose函数,会把原来的dispatch函数替换为会遍历执行所有的middleware的新的dispatch函数,这样每次执行dispatch时都会经过一次次的middlewares的调用,最终会生成新的dispatch,并return出去。
来看源码,我看了好几遍也看不懂,感兴趣的可以深入研究一下。
以上介绍了createStore中的第三个参数enhancer的中的第一个参数applyMiddleware,我们来看第二个参数enhancers,devToolsExtension是一个观察redux中状态变化的一个谷歌插件。
if (__DEV__) { |
通过compose函数把多个增强器:中间件和调试工具组合,嵌套执行。
以上就介绍完了createStore中传入的第三个参数,那这个参数是干什么用的呢,上面介绍了对middlerware的强化,它是对createStore方法进行强化的,返回功能加强后的新的store。
三、使用store
- 在main.js文件中,引入上面创建好的createStore函数,进行实例化,创建好的store会暴露出一些API。
- getState()用来获取应用的state树,
- subscribe()是是一个变化监听器,在dispatch action之后,state会发生变化,在它的回调函数中,通过getState()重新获取state,赋值给全局变量。
- 同时还要引入创建好的路由,
- 把store和routes传入项目的根组件AppContainer,挂载到创建好的dom节点上。
下面来具体看一下AppContainer组件
引入的库如下:
import PropTypes from 'prop-types'; import React, { Component } from 'react'; |
1. PropTypes是react内置的类型检查工具,用来对组件上的props进行类型检查。
使用方法如下:
项目中要求传入AppContainer的对象是必填的。
AppContainer.propTypes = { |
2. react router是基于history的,history监听浏览器地址栏的变化,然后router根据它来匹配路由,从而渲染正确的组件。
它有三种形式,browserHistory、hashHistory、createMemoryHistory,项目中使用的是browserHistory,它创建的URL是类似于example.com/some/path。
3.接下来看最关键的Provider,通过它包裹整个项目的根组件,并且传入Store对象,在相应的容器组件内才能通过connect来结合React和Redux。
关键之处就在于connect()函数,用法如下:
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]) |
- 第一个参数:mapStateToProps(state, [ownProps]): stateProps (Function)
根据名字来看,是对state到props做一个映射,这个函数接收两个参数,第一个参数是应用的state,第二个参数顾名思义指的是本组件的props,函数必须返回一个纯对象,这个对象就和组件的props组合,形成新的props,每当state变化,或者组件中的props变化的时候,mapStateToProps都会被调用。
例如组件ReduxForm中:
对本组件的props进行处理,获取到state中相应的数据,然后赋值给formReducerVal,那么组件的props就会多出这个属性。
- 第二个参数:mapDispatchToProps(dispatch, [ownProps]): dispatchProps (Object or Function)
根据名字意思就是把dispatch action也映射到组件的props上,如同把state映射到props上一样。
通常我们调用action时,需要由store上的dispatch来触发,下面的例子就把aciton和dispatch合成了一个值,作为当前组件的属性,使得可以在组件中通过this.props的方式访问action
const mapDispatchToProps = (dispatch, ownProps) => { this.props.increase(); this.props.decease(); |
redux官方给提供了一个现成的方法,可以实现同样的效果。
const mapDispatchToProps = (dispatch, ownProps) => { |
在我们的项目中是这样使用的,所以在组件中直接通过 this.props.dispatch的方式调用action,并没有进行过多的处理。
const mapDispatchToProps = (dispatch) => {
const mapDispatchToProps = (dispatch) => { return { ...bindActionCreators(actions,dispatch), dispatch } } |
- 第三个参数 mergeProps(stateProps, dispatchProps, ownProps): props (Function)
它是接收前面两个参数的执行结果的回调函数,并会把返回值和当前的props合并
- 第四个参数options (Object) 用来定制 connector 的行为
以上就完成了react使用redux的基本步骤。