理解Redux中间件,这一篇就够了

前言

最近对项目中使用的Redux中间件进行了梳理,在此过程当中对Redux中间件的原理也有了较为深入的理解。因此抽空总结了一下,也但愿能给读者们带来一些启发。html

这篇文章会介绍Redux中间件的工做机制,并经过源码分析来了解其原理,最后会经过一个例子介绍如何编写Redux中间件。git

Redux中间件工做机制

用过Redux中间件的朋友都知道redux给咱们带来了强大的数据管理功能,然而Redux的强大之处还在于其能够经过扩展中间件来加强和丰富其功能。 其实,Redux中间件的功能就是提供一个自定义处理Action的方式,在middleware中咱们能够对流入的action进行筛选,对于选中的action进行一些自定义的操做,最终生成一个新的action并派发(dispatch)下去。 下面经过两张图来对比一下是否使用Redux中间件时的差别。 首先是不使用中间件的redux数据流:redux

不使用middleware时,在dispatch(action)时会直接送到reducer执行,并根据action的type更新返回相应的state。但在复杂得业务场景下,这种简单的机制很难知足咱们的需求。

经过增长中间件,咱们就能够在action到达reducer以前对action进行拦截处理。而且middleware是能够自由组合的插件机制,这能够方便咱们编写不一样的middleware,并按照必定顺序组合这些middleware来知足咱们的业务场景需求。简单来讲,这个middleware的机制就是对dispatch进行了加强(enhance)。

middleware源码分析

通常咱们在业务中会这样添加中间件:api

import { applyMiddleware, createStore } from 'redux';
import thunk from "redux-thunk"; 
import createLogger from 'redux-logger';
const logger = createLogger();

const middleware = [thunk, logger];
const store = createStore(rootReducers, applyMiddleware(...middleware));
复制代码

重点关注createStore和applyMiddleware这两个方法。数组

createStore

function createStore(reducer, preloadedState, enhancer) {
  ...
  
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.')
    }
    
    return enhancer(createStore)(reducer, preloadedState)
  }
    
  ...
    
  // When a store is created, an "INIT" action is dispatched so that every
  // reducer returns their initial state. This effectively populates
  // the initial state tree.
  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}
复制代码

createStore的代码比较长,节选了一部分。这里的enhancer参数即为applyMiddleware()返回的组合后的中间件。bash

applyMiddleware

function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }
    // 生成初始的middlewareAPI(store的基本方法)
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // 将middlewareAPI传递给中间件并执行
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 组合这些中间件,传入初始dispatch生成加强后的dispatch方法,并改变初始dispatch指向
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}
复制代码

applyMiddleware的代码更加简单,只有短短十几行代码。不难看出applyMiddleware的核心就在于组合(compose)中间件。网络

在分析applyMiddleware以前,咱们有必要先看一下一个标准的middleware的写法:app

const standardMidddleware = store => next => action => next(action)
复制代码

上面这个方法接受一个store参数,也就是上面applyMiddleware源码里的middlewareAPI对象。standardMidddleware方法返回的函数接受一个next参数,这个next也是一个函数,当next执行的时候即表示当前middleware的工做结束,将action交给下一个中间件执行。异步

咱们继续梳理applyMiddleware的流程,能够将其分红下面几个步骤:async

一、执行各个middleware,生成一个chain数组

将store的基本方法做为一个对象参数,传递给中间件并依次执行,合并到一个chain数组中。此时chain数组结构以下:

[
    next => action => { doSomething1(); return next(action) },
    next => action => { doSomething2(); return next(action) },
    next => action => { doSomething3(); return next(action) },
    ...
]
复制代码

二、利用compose方法将chain数组中的中间件进行组合

compose方法定义以下:

function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)), v=>v)
}
复制代码

middleware的设计采用了柯里化的方式,这样就便于compose,从而能够动态生成next方法。applyMiddleware方法中最关键的代码也就是下面这行代码:

dispatch = compose(...chain)(store.dispatch)
复制代码

等价于

dispatch = f1(f2(f3(store.dispatch))))
复制代码

compose就是这样将middleware一层层嵌套包裹到原始的dispatch 方法上,获得一个加强后的dispatch方法。

Redux中间件实践

举一个日常开发中可能遇到的一个例子:

咱们在发起网络请求以前会让页面上出现loading的动画,完成请求以后再让loading消失。

在不使用Redux时咱们很容易办到:

this.setState({ loading: true });
this.fetchData().then(
    resp => {
        ...
        this.setState({ loading: false });
    }
);
复制代码

但当咱们使用redux以后,咱们会在异步操做中发起网络请求,并将数据存储到Redux store中。因为网络请求被包裹在Redux的异步操做中,咱们彷佛就无法获取弹出loading的时机了。

这时,Redux的中间件就派上用场了。

对于Redux的同步操做,咱们只须要dispatch一种Action便可;可是对于异步操做,须要处理三种Action:

  • 操做发起时的 Action
  • 操做成功时的 Action
  • 操做失败时的 Action

对于发送网络请求的异步操做,对应的三种Action类型能够定义以下:

const actionTypes = {
  FETCH_xxx_REQUEST: "FETCH_xxx_REQUEST",
  FETCH_xxx_SUCCESS: "FETCH_xxx_SUCCESS",
  FETCH_xxx_FAILURE: "FETCH_xxx_FAILURE"
};
复制代码

Reducer方法即为

const Reducer = (state = initialState, action) => {
  switch(action.type) {
    case types.FETCH_xxx_REQUEST:
      return { ...state, isFetching: true };
    case types.FETCH_xxx_SUCCESS:
      return {
        ...state,
        isFetching: false,
        resp: action.payload
      };
    case types.FETCH_xxx_FAILURE:
      return { 
        ...state, 
        isFetching: false, 
        error: action.error 
      };
    default:
      return state;
  }
}
复制代码

能够发现这里使用State的属性isFetching来标识是否在抓取数据。

建立对应的ActionCreator方法返回值须要包含上述的三种action类型:

fetchxxx: () => (dispatch) => {
    const endpoint = urlMap.fetchxxx;
    return dispatch({
      [FETCH_DATA]: {
        types: [
          types.FETCH_xxx_REQUEST,
          types.FETCH_xxx_SUCCESS,
          types.FETCH_xxx_FAILURE
        ],
        endpoint,
      }
    })
  }
复制代码

能够发现返回的action对象跟常规的action 有所不一样,而且全部内容都包裹在[FETCH_DATA]对象里面,而这个FETCH_DATA字段也正是用以在中间件中与其余普通action相区分。

这样,咱们须要的中间件也就呼之欲出了:

const apiMiddleware = store => next => action => {
  const callAPI = action[FETCH_DATA]
  // 若是action不包含FETCH_DATA字段,直接交给下一个中间件执行。
  if (typeof callAPI === 'undefined') {
    return next(action)
  }

  let { endpoint } = callAPI
  const { types, body } = callAPI

  if (typeof endpoint !== 'string') {
    throw new Error('Specify a string endpoint URL.')
  }
  
  if (!Array.isArray(types) || types.length !== 3) {
    throw new Error('Expected an array of three action types.')
  }
  if (!types.every(type => typeof type === 'string')) {
    throw new Error('Expected action types to be strings.')
  }

  const actionWith = data => {
    const finalAction = {...action, ...data}
    delete finalAction[FETCH_DATA]
    return finalAction
  }

  const [ requestType, successType, failureType ] = types
  // 发送请求以前,发出类型为‘请求中’的action
  next(actionWith({ type: requestType }))

  return doRequest(endpoint, body).then(
    // 请求成功以后,发出类型为‘请求成功’的action
    response => next(actionWith({
      payload: response,
      type: successType
    })),
    // 请求失败以后,发出类型为‘请求失败’的action
    error => next(actionWith({
      type: failureType,
      error: error.message || 'Something bad happened'
    }))
  )
}
复制代码

上述用于处理网络请求的middleware,能够分红如下几个步骤:

一、判断action是否包含FETCH_DATA字段,若是不包含FETCH_DATA则直接交给下一个中间件执行;

二、在发送请求以前,dispatch类型为‘请求中’的action;

三、请求返回时,若是请求成功,dispatch类型为‘请求成功’的action;不然dispatch类型为‘请求失败’的action。

在上述过程当中,发出任一类型的action以后,咱们就能获取isFetching状态变化了。

到此,咱们须要的中间件就完成了。

(完)

Reference

相关文章
相关标签/搜索