理解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

相關文章
相關標籤/搜索