我们在使用react 的状态管理 redux的时候,必不可少的需要使用到中间件,做个笔记总结一下关于个人使用这两个中间件的心得。
什么是中间件
具体的介绍我也不知道怎么说,看阮一峰老师的文章,几行代码,你就能立马了解到了:
let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action);
next(action);
console.log('next state', store.getState());
}
上面代码中,对store.dispatch进行了重定义,在发送 Action 前后添加了打印功能。这就是中间件的雏形。
中间件就是一个函数,对store.dispatch方法进行了改造,在发出 Action 和执行 Reducer 这两步之间,添加了其他功能。
其他的相关介绍可以去看阮一峰老师的这篇文章:www.cnblogs.com/chaoyuehedy…
为什么要使用中间件
redux中的数据流大致是: UI—————>action(plain)—————>reducer——————>state——————>UI
redux是遵循函数式编程的规则,上述的数据流中,action是一个原始js对象(plain object)且reducer是一个纯函数,对于同步且没有副作用的操作,上述的数据流起到可以管理数据,从而控制视图层更新的目的。
但是如果存在副作用,比如ajax异步请求等等,那么应该怎么做?
如果存在副作用函数,那么我们需要首先处理副作用函数,然后生成原始的js对象。如何处理副作用操作,在redux中选择在发出action,到reducer处理函数之间使用中间件处理副作用。 redux增加中间件处理副作用后的数据流大致如下:
UI——>action(side function)—>middleware—>action(plain)—>reducer—>state—>UI
在有副作用的action和原始的action之间增加中间件处理,从上面我们也可以看出,中间件的作用就是:
转换异步操作,生成原始的action,这样,reducer函数就能处理相应的action,从而改变state,更新UI。
redux-thunk的使用
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
上面是thunk的中间件核心代码,这几行代码做的就是判断action的类型,如果action是函数,就调用这个函数,调用的步骤为:
action(dispatch, getState, extraArgument);
发现实参为dispatch和getState,因此我们在定义action为thunk函数是,一般形参为dispatch和getState。 如果不是就使用原始的action。
redux-thunk的大体过程:
action1(side function)—>redux-thunk监听—>执行相应的有副作用的方法—>action2(plain object)
这样我们就能在代码种这样使用了:
class AsyncApp extends Component {
componentDidMount() {
const { dispatch, selectedPost } = this.props
dispatch(fetchPosts(selectedPost))
}
直接在UI层 dispatch一个我们请求数据的的接口, 在fetchPoast中:
const fetchPosts = postTitle => (dispatch, getState) => {
dispatch(requestPosts(postTitle));
return fetch(`url`)
.then(response => response.json())
.then(json => dispatch(action));
};
};
上面代码中,fetchPosts是一个Action Creator(动作生成器),返回一个函数。这个函数执行后,先发出一个Action(requestPosts(postTitle)),然后进行异步操作。拿到结果后,先将结果转成 JSON 格式,然后再发出一个 Action( receivePosts(postTitle, json))。 下面就是一个 例子:
export function getOrderList() {
return (dispatch, getState) => {
let userNo = getState().user.userid;
post(API.getOrderList, { userNo })
.then(data => {
dispatch({
type: actionTypes.GET_ORDER_LIST,
data: data.data
})
})
}
}
这样我们就能在reducer中接受到数据—>更新state—>更新UI。 也就是说react-thunk其实就是复写了dispatch方法。让我们可以更方便的操作,也可以异步发出action。
缺点
react-thunk仅仅做了执行这个函数,并不在乎函数主体内是什么,也就是说thunk使得redux可以接受函数作为action,但是函数的内部可以多种多样。action不易维护。
- action不易维护的原因:
- action的形式不统一
- 就是异步操作太为分散,分散在了各个action中
redux-saga的使用
跟redux-thunk,redux-saga是控制执行的generator,在redux-saga中action是原始的js对象,把所有的异步副作用操作放在了saga函数里面。这样既统一了action的形式,又使得异步操作集中可以被集中处理。
redux-saga是通过genetator实现的,如果不支持generator需要通过插件babel-polyfill转义。我们接着来实现一个输出login的例子。
- redux-saga的大体过程如下:
action1(plain object)——>redux-saga监听—>执行相应的Effect方法——>返回描述对象—>恢复执行异步和副作用函数—>action2(plain object)
- action 统一的action形式
import * as types from '../constants/ActionTypes';
// import console = require('console');
export function requestLogin(mobile, password) {
return {
type: types.REQUEST_LOGIN,
mobile,
password
};
}
export function fetchLogin() {
return {
type: types.FETCH_LOGIN
};
}
export function receiveLogin(userInfo, token) {
return {
type: types.RECEIVE_LOGIN,
userInfo,
token
};
}
2.saga 监听 返回action
// 监听take(action) 当响应到对应的type的action时执行
export function* watchRequestLogin() {
while (true) {
const { mobile, password } = yield take(types.REQUEST_LOGIN);
yield fork(requestLogin, mobile, password);
}
}
// 异步的方法 主要业务逻辑 必须要put出action 。
export function* requestLogin(mobile, password) {
try {
const loginParam = { userName: mobile, passWord: password, token: '' };
yield put(fetchLogin());
const data = yield call(NetWork.post, API_CONFIG.userAppLogin, loginParam);
const SystemCode = data.SystemCode;
if (SystemCode !== 1) {
console.log(SystemCode);
yield put(receiveLogin({}, ''));
yield Toast.fail(ERROR_CONFIG[SystemCode] ? ERROR_CONFIG[SystemCode] : `未知错误!Code=${SystemCode}`);
} else {
const userInfo = data;
const token = data.token;
yield put(receiveLogin(userInfo, token));
}
} catch (error) {
yield put(receiveLogin({}, ''));
yield Toast.fail(ERROR_CONFIG[error.SystemCode] ? ERROR_CONFIG[error.SystemCode] : `未知错误!Code=${error.SystemCode}`);
}
}
3.reducers
const initstate={
login:true
}
export default function login(state = initialState, action) {
switch (action.type) {
case types.FETCH_LOGIN:
return Object.assign({}, state, {
loading: true
});
default:
return state;
}
}
- ui component中使用
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as loginCreators from '../actions/login';
class LoginContainer extends Component {
login(){
const { mobile, password } = this.state;
const { loginActions } = this.props;
loginActions.requestLogin(mobile, password);
}
}
const mapStateToProps = (state) => {
const { login } = state;
return {
login
};
};
const mapDispatchToProps = (dispatch) => {
const loginActions = bindActionCreators(loginCreators, dispatch);
return {
loginActions
};
};
export default connect(mapStateToProps, mapDispatchToProps)(LoginContainer);
react-saga的优点
redux-saga除了上述的action统一、可以集中处理异步操作等优点外,redux-saga中使用声明式的Effect以及提供了更加细腻的控制流。
-
首先,在redux-saga中提供了一系列的api,比如take、put、all、select等API ,在redux-saga中将这一系列的api都定义为Effect。这些Effect执行后,当函数resolve时返回一个描述对象,然后redux-saga中间件根据这个描述对象恢复执行generator中的函数。
-
对比redux-thunk我们发现,redux-saga中监听到了原始js对象action,并不会马上执行副作用操作,会先通过Effect方法将其转化成一个描述对象,然后再将描述对象,作为标识,再恢复执行副作用函数。 通过使用Effect类函数,可以方便单元测试,我们不需要测试副作用函数的返回结果。只需要比较执行Effect方法后返回的描述对象,与我们所期望的描述对象是否相同即可。
举例来说,call方法是一个Effect类方法:
import { call } from 'redux-saga/effects'
function* fetchProducts() { const products = yield call(Api.fetch, '/products') // ... } 上述代码中,比如我们需要测试Api.fetch返回的结果是否符合预期,通过调用call方法,返回一个描述对象。这个描述对象包含了所需要调用的方法和执行方法时的实际参数,我们认为只要描述对象相同,也就是说只要调用的方法和执行该方法时的实际参数相同,就认为最后执行的结果肯定是满足预期的,这样可以方便的进行单元测试,不需要模拟Api.fetch函数的具体返回结果。
3.Effect提供的具体方法
下面来介绍几个Effect中常用的几个方法,从低阶的API,比如take,call(apply),fork,put,select等,以及高阶API,比如takeEvery和takeLatest等
-
take
take这个方法,是用来监听action,返回的是监听到的action对象。
比如: 定义一个action:
const loginAction = {
type:'login',
data: data
}
在UI Component中dispatch一个action:
dispatch(loginAction)
在saga中使用:
const action = yield take('login');
可以监听到UI传递到中间件的Action,上述take方法的返回,就是dipath的原始对象。一旦监听到login动作,返回的action为:
{
type:'login',
data: data
}
-
call(apply)
call和apply方法与js中的call和apply相似,我们以call方法为例:
call(fn, ...args) call方法调用fn,参数为args,返回一个描述对象。不过这里call方法传入的函数fn可以是普通函数,也可以是generator。call方法应用很广泛,在redux-saga中使用异步请求等常用call方法来实现。
yield call(fetch,'/userInfo',username)
- put
从工作流中,我们发现redux-saga执行完副作用函数后,必须发出action,然后这个action被reducer监听,从而达到更新state的目的。相应的这里的put对应与redux中的dispatch
- select
put方法与redux中的dispatch相对应,同样的如果我们想在中间件中获取state,那么需要使用select。select方法对应的是redux中的getState,用户获取store中的state
- fork
fork方法相当于web work,fork方法不会阻塞主线程,在非阻塞调用中十分有用。
- takeEvery和takeLatest
takeEvery和takeLatest用于监听相应的动作并执行相应的方法,是构建在take和fork上面的高阶api,比如要监听login动作,好用takeEvery方法可以:
takeEvery('login',loginFunc)
takeEvery监听到login的动作,就会执行loginFunc方法,除此之外,takeEvery可以同时监听到多个相同的action。
takeLatest方法跟takeEvery是相同方式调用:
takeLatest('login',loginFunc)
与takeLatest不同的是,takeLatest是会监听执行最近的那个被触发的action。
参考文章: