[教學] Redux Middleware Chain

December 26, 2016

分類標籤:react

這篇教學主要是整理Redux middleware的原理,如何實作,以及如何理解middleware chain的順序。

目錄

Middleware

Redux提供了middleware的機制,可以讓我們把原有的dispatch加上想要的功能。比方說:印Log,支援各種形式的action。(Redux最初的設計中,action是一個plain object)

middleware可以用store.dispatch當作原料,製造出一個新的patch過後的dispatch

如何實作Middleware

logger

假設我們想要每次dispatch的時候都印一條log,可以用middleware產生一個新的dispatch,功能是在原有的dispatch之外加上log:

const logger = (store) => {
  const rawDispatch = store.dispatch

  // Patched dispatch
  return (action) => {
    console.group(action.type)
    console.log('prev state', store.getState())
    console.log('action', action);
    const returnValue = rawDispatch(action)
    console.log('next state', store.getState());
    console.groupEnd(action.type)

    return returnValue
  }
}

上面這個middleware做的事情是:

  1. store.dispatch讀出來存在rawDispatch裡。
  2. 回傳了一個patch過版本的dispatch,也就是action => {...},當這個dispatch被呼叫時,將會印一些log,再呼叫原本的rawDispatch

當我們要使用這個middleware時,用patch過的dipatch覆蓋掉原本的store.dispatch:

store.dispatch = logger(store)

Redux Promise

假設我們的action需要打API,回傳以後根據取得的資料發送某個action,這整組動作可以被看成是一個promise物件。

// promise as an action
export const fetchTodos = (filter) =>
  api.fetchTodos(filter).then(response => ({
    type: 'RECEIVE_TODO',
    filter,
    response
  }))

如果可以直接dispatch這個promise,可以讓程式變得更簡潔。我們可以寫一個promise middleware來做到對promise的支持:

const promise = (store) => {
  const rawDispatch = store.dispatch
  return (action) => {
    if (typeof action.then === 'function') {
      return action.then(rawDispatch)
    }

    return rawDispatch(action)
  }
}

上面這個middleware做的事情是:

  1. store.dispatch讀出來存在rawDispatch裡。
  2. 回傳了一個patch過版本的dispatch,也就是action => {...},當這個dispatch被呼叫時,將會檢查action是否為promise(檢查的方法就是看看action物件有沒有名為then的function),是的話就呼叫rawDispatch去dispatch promise的回傳值。
  3. 如果是普通的action就呼叫原本的rawDispatch

當我們要使用這個middleware時,用patch過的dipatch覆蓋掉原本的store.dispatch:

store.dispatch = promise(store)

Apply middlewares

綜合以上,要使用兩個middlewares,需要複寫store.dispatch兩次:

store.dispatch = logger(store)
store.dispatch = promise(store)

Middleware chain的順序與next

為了方便理解,middleware的順序可以簡單理解成:action在middleware chain之間傳遞的順序。舉本篇的例子,會先判斷action是否屬於promise,再來logging,最後才是原本的dispatch,所以middlewares的順序會是[promise, logger]

next

為了方便理解,對所有的middlewares,我們把rawDispatch重新命名成next

const logger = store => {
  const next = store.dispatch
  return action => {
    console.group(action.type)
    console.log('prev state', store.getState())
    console.log('action', action);
    const returnValue = next(action)
    console.log('next state', store.getState());
    console.groupEnd(action.type)
    return returnValue
  }
}

const promise = (store) => {
  const next = store.dispatch
  return (action) => {
    if (typeof action.then === 'function') {
      return action.then(next)
    }

    return next(action)
  }
}

決定把rawDispatch重新命名成next的原因是:rawDispatch就是middleware chain中的『下一個』dispatch。如何理解這件事呢?

根據上面的定義,middleware chain的順序是[promise, logger]

實際上,apply middleware的順序是先做store.dispatch = logger(store)(store.dispatch),再做store.dispatch = promise(store)(store.dispatch)。結果就是在promise裡面的rawDispatchlogger回傳的dispatch,在logger裡面的rawDispatch是原版的store.dispatch

按照[promise, logger]的順序,rawDispatch自然就會變成middleware chain中的下一個dispatch,例如:promise裡的rawDispatch logger回傳的dispatch。而loggerrawDispatch 是原版的store.dispatch

所以我們可以給予rawDispatch一個更容易理解的稱呼:next

Apply middlewares

我們可以統一用一個wrapDispatchWithMiddlewares函數處理middlewares。他的功能是:對每個middleware,按照順序覆寫store.dispatch

const middlewares = [promise, logger]
wrapDispatchWithMiddlewares(store, middlewares)

const wrapDispatchWithMiddlewares = (store, middlewares) => {
  middlewares.slice().reverse().forEach(middleware => {
    store.dispatch = middleware(store)
  })
}

每個middleware中都會重複做const next = store.dispatch,所以我們可以把next抽出來,改寫成curry function的形式(curry function可以想成:一樣是function,只是他的變數可以分階段apply):

const logger = store => next => action => {
  console.group(action.type)
  console.log('prev state', store.getState())
  console.log('action', action);
  const returnValue = next(action)
  console.log('next state', store.getState());
  console.groupEnd(action.type)

  return returnValue
}

const promise = store => next => action => {
  if (typeof action.then === 'function') {
    return action.then(next)
  }
  return next(action)
}

呼叫時將next參數用store.dispatch帶入:

const wrapDispatchWithMiddlewares = (store, middlewares) => {
  middlewares.slice().reverse().forEach(middleware => {
    store.dispatch = middleware(store)(store.dispatch)
  })
}

因為middleware chain順序為[promise, logger],但需要先applylogger再applypromise的緣故,wrapDispatchWithMiddlewares()裡需要先對middlewaresreverse()

在這個迴圈裡面會apply兩次middleware:

  1. 第一次迴圈執行,相當於store.dispatch = logger(store)(store.dispatch)logger接受原始的dispatch並回傳有log的版本。然後store.dispatch被覆寫成有log的版本。
  2. 第二次迴圈執行,相當於store.dispatch = promise(store)(store.dispatch)promise接受有log的dispatch,並且回傳有log同時支援promise的版本。最後store.dispatch被覆寫成有log同時支援promise的版本。

Remove Monkeypatching

為了不要動到原本的dispatch API,我們可以回傳store的copy,其中dispatch是被修改過後的版本:

const applyMiddleware = (store, middlewares) => {
  let dispatch = store.dispatch
  middlewares.slice().reverse().forEach(middleware => {
    dispatch = middleware(store)(dispatch)
  })

  // Return a copy of store where dispatch() is patched
  return {...store, dispatch}
}

applyMiddleware API

上面的applyMiddleware版本並不應該在真實的環境中使用,只是為了更加理解middleware,實際上redux原生就提供了applyMiddleware API。

實際的程式碼並不長,但這段程式碼其實大有學問:

export default function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState, enhancer) => {
    var store = createStore(reducer, preloadedState, enhancer)
    var dispatch = store.dispatch
    var chain = []

    var middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

middleware的形式為({dispatch, getState}) => next => action => {...},所以第一步是將getStatedispatch注入middleware:

var middlewareAPI = {
  getState: store.getState,
  dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))

下一步是將next注入middleware,這裡用到了compose API,其功能顧名思義是將一連串的function組合起來,例如:compose(f, g, h)會回傳(...args) => f(g(h(...args)))

不斷地把下一個middleware回傳的function當作next參數注入當前的middleware,一層一層把store.dispatch包起來以後,就可以得到最終版的dispatch

dispatch = compose(...chain)(store.dispatch)

最後有一個很重要的關鍵,就是被注入的middleware.dispatch實際上用了閉包的形式,也就是action => dispatch存了dispatch這個變數,並且在最後用dispatch = compose(...chain)(store.dispatch)改寫dispatch這個變數,也就是說實際上被注入middleware的dispatch變數是加上所有middleware功能的最終版

var dispatch = store.dispatch
...
middlewareAPI = {
  ...,
  dispatch: (action) => dispatch(action)
}
...
dispatch = compose(...chain)(store.dispatch)

總結以上,原生版本的applyMiddleware跟我們自己寫的差異主要有以下幾點:

  1. 在middleware裡面看到的dispatch對應到加上所有middleware功能的最終版dispatch
  2. 在middleware裡面會多看到getState,對應到redux原生的getState
  3. next對應到middleware chain中的下一個middleware。

在官方文件對於middleware的說明中,可以看到以下說明:

It does a bit of trickery to make sure that if you call store.dispatch(action) from your middleware instead of next(action), the action will actually travel the whole middleware chain again, including the current middleware. This is useful for asynchronous middleware, as we have seen previously.

我想所謂的trickery,指的就是將擁有所有middleware功能的dispatch注入至middleware,如此一來透過dispatch呼叫的action都必然會完整經過middleware chain的每一環。

redux-thunk中就運用了類似的特性。thunk是一種利用function的形式來表示一連串的async actions:

const someAsyncAction = () => (dispatch, getState) => {
  dispatch(anotherAsyncAction())
}

dispatch(someAsyncAction())

thunk middleware的實作大致上是將dispatch注入到thunk中,讓thunk自行控制內部非同步機制如何實作:

const thunkMiddleware = ({ dispatch, getState }) => next => action => {
  if (typeof action === 'function') {
    return action(dispatch, getState)
  }

  return next(action);
}

thunk最方便的功能之一就是thunk裡面可以dispatch其他的async thunk,靠的就是applyMiddleware的實作中注入了有全部middleware功能的dispatch,保證任何被dispatch的thunk一定會被thunk middleware處理到。

API Usage

import { createStore, applyMiddleware } from 'redux';
import createLogger from 'redux-logger'
import promise from 'redux-promise'

const configureStore = () => {
  const middlewares = [promise]
  if (process.env.NODE_ENV !== 'production') {
    middlewares.push(createLogger())
  }

  const store = createStore(
    todoApp,
    applyMiddleware(...middlewares) // optional enhancer
  );

  return store
}

參考資料

redux.js.org

Egghead

深入理解Redux的Middleware

Understanding Redux Middleware


Profile picture

Shubo Chao 軟體工程師,目前大多專注於前端開發