深入淺出redux-middleware

我就什麼話也不說發表於2018-12-11

多數redux初學者都會使用redux-thunk這個中介軟體來處理非同步請求(比如我)

本來寫這篇文章只是想寫寫redux-thunk,然後發現還不夠,就順便把middleware給過了一遍。

為什麼叫thunk?

thunk是一種包裹一些稍後執行的表示式的函式。

redux-thunk原始碼

所有的程式碼就只有15行,我說的是真的。。 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;
複製程式碼

程式碼很精簡,但是功能強大,所以非常有必要去了解一下。

redux-middleware是個啥

深入淺出redux-middleware

上圖描述了一個redux中簡單的同步資料流動的場景,點選button後,dispatch一個action,reducer 收到 action 後,更新state後告訴UI,幫我重新渲染一下。

redux-middleware就是讓我們在dispatch action之後,在action到達reducer之前,再做一點微小的工作,比如列印一下日誌什麼的。試想一下,如果不用middleware要怎麼做,最navie的方法就是每次在呼叫store.dispatch(action)的時候,都console.log一下actionnext State

store.dispatch(addTodo('Use Redux'));
複製程式碼
  • naive的方法,唉,每次都寫上吧
const action = addTodo('Use Redux');

console.log('dispatching', action);
store.dispatch(action);
console.log('next state', store.getState());
複製程式碼
  • 既然每次都差不多,那封裝一下吧
function dispatchAndLog(store, action) {
  console.log('dispatching', action);
  store.dispatch(action);
  console.log('next state', store.getState());
}
複製程式碼
  • 現在問題來了,每次dispatch的時候都要import這個函式進來,有點麻煩是不是,那怎麼辦呢?

既然dispatch是逃不走的,那就在這裡動下手腳,reduxstore就是一個有幾種方法的物件,那我們就簡單修改一下dispatch方法。

const next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action);
  next(action); // 之前是 `dispatch(action)`
  console.log('next state', store.getState());
}
複製程式碼

這樣一來我們無論在哪裡dispatch一個action,都能實現想要的功能了,這就是中介軟體的雛形。

深入淺出redux-middleware

  • 現在問題又來了,大佬要讓你加一個功能咋辦?比如要異常處理一下

接下來就是怎麼加入多箇中介軟體了。

function patchStoreToAddLogging(store) {
  const next = store.dispatch
  store.dispatch = function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}

function patchStoreToAddCrashReporting(store) {
  const next = store.dispatch
  store.dispatch = function dispatchAndReportErrors(action) {
    try {
      return next(action)
    } catch (err) {
      console.error('Caught an exception!', err)
      Raven.captureException(err, {
        extra: {
          action,
          state: store.getState()
        }
      })
      throw err
    }
  }
}
複製程式碼

patchStoreToAddLoggingpatchStoreToAddCrashReportingdispatch進行了重寫,依次呼叫這個兩個函式之後,就能實現列印日誌和異常處理的功能。

patchStoreToAddLogging(store)
patchStoreToAddCrashReporting(store)
複製程式碼
  • 之前我們寫了一個函式來代替了store.dispatch。如果直接返回一個新的dispatch函式呢?
function logger(store) {
  const next = store.dispatch

  // 之前:
  // store.dispatch = function dispatchAndLog(action) {

  return function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}
複製程式碼

這樣寫的話我們就需要讓store.dispatch等於這個新返回的函式,再另外寫一個函式,把上面兩個middleware連線起來。

function applyMiddlewareByMonkeypatching(store, middlewares) {
  middlewares = middlewares.slice()
  middlewares.reverse()

  // Transform dispatch function with each middleware.
  middlewares.forEach(middleware => (store.dispatch = middleware(store)))
}
複製程式碼

middleware(store)會返回一個新的函式,賦值給store.dispatch,下一個middleware就能拿到一個的結果。

接下來就可以這樣使用了,是不是優雅了一些。

applyMiddlewareByMonkeypatching(store, [logger, crashReporter])
複製程式碼

我們為什麼還要重寫dispatch呢?當然啦,因為這樣每個中介軟體都可以訪問或者呼叫之前封裝過的store.dispatch,不然下一個middleware就拿不到最新的dispatch了。

function logger(store) {
  // Must point to the function returned by the previous middleware:
  const next = store.dispatch

  return function dispatchAndLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}
複製程式碼

連線middleware是很有必要的。

但是還有別的辦法,通過柯里化的形式,middlewaredispatch作為一個叫next的引數傳入,而不是直接從store裡拿。

function logger(store) {
  return function wrapDispatchToAddLogging(next) {
    return function dispatchAndLog(action) {
      console.log('dispatching', action)
      let result = next(action)
      console.log('next state', store.getState())
      return result
    }
  }
}
複製程式碼

柯里化就是把接受多個引數的函式程式設計接受一個單一引數(注意是單一引數)的函式,並返回接受餘下的引數且返回一個新的函式。

舉個例子:

const sum = (a, b, c) => a + b + c;

// Curring
const sum = a => b => c => a + b + c;
複製程式碼

ES6的箭頭函式,看起來更加舒服。

const logger = store => next => action => {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

const crashReporter = store => next => action => {
  try {
    return next(action)
  } catch (err) {
    console.error('Caught an exception!', err)
    Raven.captureException(err, {
      extra: {
        action,
        state: store.getState()
      }
    })
    throw err
  }
}
複製程式碼

接下來我們就可以寫一個applyMiddleware了。

// 注意:這是簡單的實現
function applyMiddleware(store, middlewares) {
  middlewares = middlewares.slice()
  middlewares.reverse()
  let dispatch = store.dispatch
  middlewares.forEach(middleware => (dispatch = middleware(store)(dispatch)))
  return Object.assign({}, store, { dispatch })
}
複製程式碼

上面的方法,不用立刻對store.dispatch賦值,而是賦值給一個變數dispatch,通過dispatch = middleware(store)(dispatch)來連線。

現在來看下reduxapplyMiddleware是怎麼實現的?

applyMiddleware

/**
 * Composes single-argument functions from right to left. The rightmost
 * function can take multiple arguments as it provides the signature for
 * the resulting composite function.
 *
 * @param {...Function} funcs The functions to compose.
 * @returns {Function} A function obtained by composing the argument functions
 * from right to left. For example, compose(f, g, h) is identical to doing
 * (...args) => f(g(h(...args))).
 */
 
 // 就是把上一個函式的返回結果作為下一個函式的引數傳入, compose(f, g, h)和(...args) => f(g(h(...args)))等效

export default 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)))
}
複製程式碼

compose最後返回的也是一個函式,接收一個引數args

export default 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.`
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    
    // 確保每個`middleware`都能訪問到`getState`和`dispatch`
    
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // wrapDispatchToAddLogging(store)
    dispatch = compose(...chain)(store.dispatch)
    
    // wrapCrashReport(wrapDispatchToAddLogging(store.dispatch))

    return {
      ...store,
      dispatch
    }
  }
}

複製程式碼

深入淺出redux-middleware
借用一下大佬的圖, google搜尋redux-middleware第一張

到這裡我們來看一下applyMiddleware是怎樣在createStore中實現的。

export default function createStore(reducer, preloadedState, enhancer){
  ...
}
複製程式碼

createStore接受三個引數:reducer, initialState, enhancerenhancer就是傳入的applyMiddleware函式。

createStore-enhancer #53

//在enhancer有效的情況下,createStore會返回enhancer(createStore)(reducer, preloadedState)。
return enhancer(createStore)(reducer, preloadedState)
複製程式碼

我們來看下剛剛的applyMiddleware,是不是一下子明白了呢。

return createStore => (...args) => {
    // ....
}
複製程式碼

到這裡應該就很容易理解redux-thunk的實現了,他做的事情就是判斷action 型別是否是函式,如果是就執行action,否則就繼續傳遞action到下個 middleware

參考文件: