关于react 中间件 react-sage 和 react-thunk的使用

3,312 阅读8分钟

我们在使用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不易维护的原因:
    1. action的形式不统一
    2. 就是异步操作太为分散,分散在了各个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)

  1. 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;
  }
}
  1. 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以及提供了更加细腻的控制流。

  1. 首先,在redux-saga中提供了一系列的api,比如take、put、all、select等API ,在redux-saga中将这一系列的api都定义为Effect。这些Effect执行后,当函数resolve时返回一个描述对象,然后redux-saga中间件根据这个描述对象恢复执行generator中的函数。

  2. 对比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。

我自己画的理解图

参考文章:

react-china.org/t/redux-sag… www.cnblogs.com/chaoyuehedy…