一、什么是中间件?
顾名思义就是在一个执行流程中间用的一个组件,截住过路的然后对其进行控制增强的操作来满足我们的需求。
那redux为什么需要用中间件呢?我们看一下redux的数据流向
view -> action -> reducer -> state ->view
view触发事件触发action,action中根据type把数据传到对应的reduce中,然后reducer拿到数据返回新的state。
这流程就像从view给出一个订单,然后货车马上拿着订单根据在action的这条路上不停的开到对应的reducer中,然后在reducer中卸货,对货物处理入库,然后车子再把处理后新的货品运回view中展示。当我们的操作都是同步的时候,这个流程毫无问题,但是当我们的需要异步获取数据来更新的时候就发现有问题了,即我们的车子已经出发了,但是要给reducer运的货还需要等一下才能到,等货备好了但车子已经跑没影了。
那我们要怎么办呢加快备货吗,显然是想要加快异步操作是没有用的,毕竟车子一听到指令就开了,在开的同时备好货是来不及的,因为你的货在车外。而reducer是一个纯函数的数据处理点,是一个加工中心,人家哪里知道你的货是啥。所以最终我们要想办法拦住车子,等货备好装上车才让车子继续开往reducer,那我们就在action->reducer这个环节中间进行拦截住车子,在这个拦截住的过程中我们还可以对车子为所欲为了,比如日志控制,调用异步请求、路由控制等等。
然后我们的流程就成为如下路径
view -> action ->middlewire -> reducer -> state ->view
举完这个例子应该也大概知道为什么redux处理异步数据需要用一个中间件了,那接下来我们来看看常用的redux中间件
二、Redux-thunk
npm安装该包,然后直接将redux-thunk引入到createStore操作的文件中
1 import { applyMiddleware, createStore } from 'redux'; 2 import thunk from 'redux-thunk'; 3 const store = createStore( 4 reducers, 5 applyMiddleware(thunk) 6 );
直接将thunk中间件引入,放在applyMiddleware方法之中,传入createStore方法,就完成了store.dispatch()的功能增强。即可以在reducer中进行一些异步的操作。 applyMiddleware() 其实applyMiddleware就是Redux的一个原生方法,将所有中间件组成一个数组,依次执行。 中间件多了可以当做参数依次传进去。
redux-thunk源码:(路径node_modules/redux-thunk/src/index.js)
1 function createThunkMiddleware(extraArgument) { 2 return ({ dispatch, getState }) => next => action => { 3 if (typeof action === 'function') { 4 return action(dispatch, getState, extraArgument); 5 } 6 return next(action); 7 }; 8 } 9 const thunk = createThunkMiddleware(); 10 thunk.withExtraArgument = createThunkMiddleware; 11 export default thunk;
redux-thunk中间件export default的就是createThunkMiddleware()过的thunk,再看createThunkMiddleware这个函数,返回的是一个柯里化过的函数。我们再翻译成ES5的代码容易看一点
1 function createThunkMiddleware(extraArgument) { 2 return function({ dispatch, getState }) { 3 return function(next){ 4 return function(action){ 5 if (typeof action === 'function') { 6 return action(dispatch, getState, extraArgument); 7 } 8 return next(action); 9 }; 10 } 11 } 12 }
可以看出来redux-thunk最重要的思想,就是可以接受一个返回函数的action creator。如果这个action creator 返回的是一个函数,就执行它,如果不是,就按照原来的next(action)执行。 正因为这个action creator可以返回一个函数,那么就可以在这个函数中执行一些异步的操作。例如:
1 export function addCount() { 2 return {type: ADD_COUNT} 3 } 4 export function addCountAsync() { 5 return dispatch => { 6 setTimeout( () => { 7 dispatch(addCount()) 8 },2000) 9 } 10 }
addCountAsync函数就返回了一个函数,将dispatch作为函数的第一个参数传递进去,在函数内进行异步操作就可以了。当然为了避免在异步请求内去使用dispatch,以至于会出现回调地狱的情况,我们当然使用promise来处理,那就就得加上promiseMiddleware中间件来处理promise。为了能在更好的观察监控我们的action,可以用createLogger中间件来打印action日志。
示例代码:
store.js
1 import { createStore, applyMiddleware } from 'redux'; 2 import thunkMiddleware from 'redux-thunk'; 3 import combineReducers from '../reducers/reducers'; 4 import promiseMiddleware from 'redux-promise' 5 import { createLogger } from 'redux-logger'; 6 import createHistory from 'history/createBrowserHistory'; 7 const history = createHistory(); 8 const arr = [thunkMiddleware,promiseMiddleware]; 9 if (process.env.NODE_ENV !== 'pro') arr.push(createLogger({ 10 diff: true, 11 collapsed: true, 12 })); 13 let store = createStore(combineReducers, applyMiddleware(...arr)); 14 export default store;
action.js
1 import types from '../store/types'; 2 import { 3 get_user_info //题主是本地配的mock服务,也可以用一个定时器返回数据实现异步 4 } from '../api/userInfo' 5 export const getUserInfo = () => (dispatch) => 6 dispatch({ 7 type: types.GET_USER_INFO_REQUEST, 8 payload: Promise.resolve(get_user_info()) 9 })
reducer.js
1 import types from '../store/types'; 2 import { reducerCreators } from '../util/index'; 3 const initState = { 4 isLoading: false, 5 userInfo: {}, 6 errorMsg: '' 7 }; 8 9 export default reducerCreators(initState, { 10 [`${types.GET_USER_INFO_REQUEST}`]: (state, data) => { 11 return Object.assign({}, state, { 12 userInfo: data.data 13 }) 14 } 15 })
reducerCreators (../util/index) 重新生成reducer使其符合范式
1 export function reducerCreators (initialState, actionTypeMapList) { 2 return (state = initialState, action) => { 3 const reducerInstance = typeof actionTypeMapList === 'object' && 4 actionTypeMapList[action.type] ? 5 actionTypeMapList[action.type](state, action.payload ? 6 action.payload : {}, action.params) : 7 state; 8 return reducerInstance; 9 }; 10 }
view.js
1 import React, { Component } from 'react'; 2 import { connect } from 'react-redux'; 3 import { bindActionCreators } from 'redux' 4 import { getUserInfo } from 'actions/userInfo'; 5 class UserInfo extends Component { 6 render() { 7 const { userInfo, isLoading, errMsg } = this.props.userInfo; 8 return (<div> 9 { 10 isLoading ? '请求中' : 11 (errMsg ? errMsg : 12 <div> 13 <p>用户信息:</p> 14 <p>用户名:{userInfo.name}</p> 15 <p>介绍:{userInfo.intro}</p> 16 </div> 17 ) 18 } 19 <button onClick={() => this.props.getUserInfo()}>请求用户信息</button> 20 </div>) 21 } 22 } 23 24 const mapStateToProps = (state) => { 25 return { 26 userInfo: state.userInfo, 27 } 28 } 29 30 const mapDispatchToProps = (dispatch) => { 31 return { 32 getUserInfo: bindActionCreators(getUserInfo, dispatch) 33 } 34 } 35 36 export default connect(mapStateToProps, mapDispatchToProps)(UserInfo);
三、Redux-saga
redux-saga 是一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。
可以想像为,一个 saga 就像是应用程序中一个单独的线程,它独自负责处理副作用。 redux-saga 是一个 redux 中间件,意味着这个线程可以通过正常的 redux action 从主应用程序启动,暂停和取消,它能访问完整的 redux state,也可以 dispatch redux action。
redux-saga 使用了 ES6 的 Generator 功能,让异步的流程更易于读取,写入和测试。(如果你还不熟悉的话,这里有一些介绍性的链接) 通过这样的方式,这些异步的流程看起来就像是标准同步的 Javascript 代码。(有点像 async/await,但 Generator 还有一些更棒而且我们也需要的功能)。
你可能已经用了 redux-thunk 来处理数据的读取。不同于 redux thunk,你不会再遇到回调地狱了,你可以很容易地测试异步流程并保持你的 action 是干净的。
示例
store.js
1 import { createStore, applyMiddleware } from 'redux'; 2 import combineReducers from '../reducers/reducers'; 3 import promiseMiddleware from 'redux-promise' 4 import createSagaMiddleware from 'redux-saga' 5 import mySaga from './saga' 6 import { createLogger } from 'redux-logger'; 7 const sagaMiddleware = createSagaMiddleware(mySaga) 8 const arr = [sagaMiddleware]; 9 arr.push(promiseMiddleware) 10 if (process.env.NODE_ENV !== 'pro') arr.push(createLogger({ 11 diff: true, 12 collapsed: true, 13 })); 14 export const store = createStore(combineReducers, applyMiddleware(...arr)); 15 sagaMiddleware.run(mySaga)
这里saga也用npm安装一下,然后像和thunk一样的引入到store中,不同的是需要用createSagaMiddleware工厂函数创建一个Saga middleware,然后连接到store上,之后便使用sagaMiddleware.run(mySaga) 运行Saga。
saga.js
1 import { call, put, takeEvery, takeLatest, take } from 'redux-saga/effects' 2 import types from '../store/types' 3 function* fetchUser(action) { 4 try { 5 const data = yield action.payload 6 yield put({ type: `${action.type}_SUCCESS`, payload: data }) 7 } catch (e) { 8 yield put({ type: `${action.type}_FAILD`, message: e.message }) 9 } 10 } 11 function* mySaga() { 12 yield takeLatest(types['GET_USER_INFO'], fetchUser) 13 } 14 export default mySaga
因为saga是实现为生成器函数(Generator functions),会yield对象到redux-saga middleware。被yield的对象都是一类指令,指令可以被middleware解释执行。当 middleware 取得一个 yield 后的 Promise,middleware 会暂停 Saga,直到 Promise 完成。在这里就是会等到action中传入的异步方法执行完毕后继续执行下一步。然后调用put指令发起一个action
action.js
1 import types from '../store/types'; 2 import { 3 get_user_info 4 } from '../api/userInfo' 5 export const getUserInfo = (params) => 6 ({ 7 type: types.GET_USER_INFO, 8 payload: get_user_info() 9 })
向saga提供action的type和异步请求的结果
reducer.js
1 import types from '../store/types'; 2 import { reducerCreators } from '../util/index'; 3 const initState = { 4 isLoading: false, 5 userInfo: {}, 6 errorMsg: '' 7 }; 8 export default reducerCreators(initState, { 9 [`${types.GET_USER_INFO}_SUCCESS`]: (state, data) => { 10 return Object.assign({}, state, { 11 userInfo: data.data 12 }) 13 } 14 })
view.js
1 import React, { Component } from 'react'; 2 import { connect } from 'react-redux'; 3 import { bindActionCreators } from 'redux' 4 import { getUserInfo } from 'actions/userInfo'; 5 6 class UserInfo extends Component { 7 render() { 8 const { userInfo, isLoading, errMsg } = this.props.userInfo; 9 10 return (<div> 11 { 12 isLoading ? '请求中' : 13 (errMsg ? errMsg : 14 <div> 15 <p>用户信息:</p> 16 <p>用户名:{userInfo.name}</p> 17 <p>介绍:{userInfo.intro}</p> 18 </div> 19 ) 20 } 21 <button onClick={() => this.props.getUserInfo()}>请求用户信息</button> 22 </div>) 23 } 24 } 25 26 const mapStateToProps = (state) => { 27 return { 28 userInfo: state.userInfo, 29 } 30 } 31 32 const mapDispatchToProps = (dispatch) => { 33 return { 34 getUserInfo: bindActionCreators(getUserInfo, dispatch) 35 } 36 } 37 38 export default connect(mapStateToProps, mapDispatchToProps)(UserInfo);
四、redux-thunk与redux-saga优缺点
Redux-thunk:
优点:
- 库小量轻,代码简短
缺点:
- thunk仅仅做了执行这个函数,并不在乎函数主体内是什么,也就是说thunk使得redux可以接受函数作为action,但是函数的内部可以多种多样
- action的形式不统一
- 异步操作分散,分散在各个action中
Redux-saga
缺点:
- 太复杂,学习成本较高
优点:
- 集中处理了所有的异步操作,异步接口部分一目了然
- action是普通对象,这跟redux同步的action一模一样
- 通过Effect,方便异步接口的测试
- 通过worker 和watcher可以实现非阻塞异步调用,并且同时可以实现非阻塞调用下的事件监听
- 异步操作的流程是可以控制的,可以随时取消相应的异步操作。
(关于mobx的异步操作,主要使用了asyncawait和特有的runInAction,有兴趣的可以自行百度,很简便的用法)