接觸過 Express 的同窗對「中間件」這個名詞應該並不陌生。在 Express 中,中間件就是一些用於定製對特定請求的處理過程的函數。做爲中間件的函數是相互獨立的,能夠提供諸如記錄日誌、返回特定響應報頭、壓縮等操做。javascript
一樣的,在 Redux 中,action 對象對應於 Express 中的客戶端請求,會被 Store 中的中間件依次處理。以下圖所示:java
中間件能夠實現通用邏輯的重用,經過組合不一樣中間件能夠完成複雜功能。它具備下面特色:git
這裏採用了 AOP (面向切面編程)的思想。github
對於面向對象思想,當須要對邏輯增長擴展功能時(如發送請求前的校驗、打印日誌等),咱們只能在所在功能模塊添加額外的擴展功能;或者選擇共有類經過繼承方式調用,可是這將致使共有類的膨脹。不然,就只有將其散落在業務邏輯的各個角落,形成代碼耦合。typescript
而使用 AOP 的思想,能夠解決代碼冗餘、耦合問題。咱們能夠將擴展功能代碼單獨放入一個切面,待執行的時候纔將其載入到須要擴展功能的位置,即切點。這樣的好處是不用更改自己的業務邏輯代碼,這種經過串聯的方式傳遞調用擴展功能也是中間件的原理。編程
中間件須要有統一的接口,才能實現自由組合。每一箇中間件必須被定義成一個函數 f1
,返回一個接收 next
參數的函數 f2
,而 f2
又返回一個接收 action
參數的函數 f3
。next
參數自己也是一個函數,中間件調用 next
函數通知 Redux 處理工做已經結束,能夠將 action 對象傳遞給下一個中間件或 Reducer。redux
例如,能夠編寫一個什麼事都不作的中間件:數組
function doNothingMiddleware ({ dispatch, getState }) { return function (next) { return function (action) { return next(action) } } }
用箭頭函數進行簡化:閉包
const doNothingMiddleware = ({ dispatch, getState }) => (next) => (action) => next(action)
能夠看出,中間件經過定義函數、接收函數、返回函數,來對 action 對象進行處理。app
不論是中間件的實現,仍是中間件的使用,都是讓每一個函數的功能儘可能小,而後經過函數的嵌套組合來實現複雜功能,這是函數式編程中的重要思想。
接下來作一些擴展,實現一個能夠記錄日誌的中間件:
const logger = ({ dispatch, getState }) => next => action => { console.log('dispatching', action) let result = next(action) console.log('next state', getState()) return result }
通過這個中間件的處理,每次觸發 action 時,會首先打印出當前 action 的信息,而後調用 next(action)
,將 action 對象傳遞給下一個中間件或 Reducer,返回處理後的結果,而後獲取最新 state 並打印。
一個記錄錯誤日誌的中間件:
const crashReporter = ({ dispatch, getState }) => next => action => { try { return next(action) } catch (err) { console.error('Caught an exception!', err) throw err } }
用 try ... catch ...
將 next(action)
包裹,對於正常 action 不作任何處理,對於出錯的 action 處理將會捕獲異常,打印錯誤日誌,並將異常拋出。
在 Redux 中,經過 applyMiddleware
來使用中間件:
import { createStore, combineReducers, applyMiddleware } from 'redux' const todoApp = combineReducers(reducers) const store = createStore( todoApp, applyMiddleware(logger, crashReporter) )
經過 store.dispatch(addTodo('use redux'))
觸發一個 action,將前後被 logger
和 crashReporter
兩個中間件處理。
接下來咱們看看 Redux 是怎麼實現中間件調用的,這是 Redux 中的部分源碼:
export default function applyMiddleware( ...middlewares: Middleware[] ): StoreEnhancer<any> { return (createStore: StoreCreator) => <S, A extends AnyAction>( reducer: Reducer<S, A>, ...args: any[] ) => { const store = createStore(reducer, ...args) let dispatch: Dispatch = () => { throw new Error( 'Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.' ) } const middlewareAPI: MiddlewareAPI = { getState: store.getState, dispatch: (action, ...args) => dispatch(action, ...args) } const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose<typeof dispatch>(...chain)(store.dispatch) return { ...store, dispatch } } }
假設有三個中間件 M1,M2,M3,應用 applyMiddleware(M1, M2, M3)
將返回一個閉包函數,該函數接收 createStore
函數做爲參數,使得建立狀態樹 store 的步驟在這個閉包內執行;而後將 store
從新組裝成 middlewareAPI
做爲新的 store
,即中間件最外層函數的參數,這樣中間件就能夠根據狀態樹進行各類操做了。
對中間件處理的關鍵邏輯在於
const chain = middlewares.map(middleware => middleware(middlewareAPI)) dispatch = compose(...chain)(store.dispatch)
首先,將 applyMiddleware
函數中傳入的中間件按順序生成一個隊列 chain
,隊列中每一個元素都是中間件調用後的結果,它們都具備相同的結構 next => action => {}
。
而後,經過 compose
方法,將這些中間件隊列串聯起來。compose
是一個從右向左的嵌套包裹函數,也是函數式編程中的經常使用範式,實現以下:
export default function compose(...funcs: Function[]) { if (funcs.length === 0) { // infer the argument type so it is usable in inference down the line return <T>(arg: T) => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => (...args: any) => a(b(...args))) }
假設 chain
是包含 C一、C二、C3(對應 M1,M2,M3 第一層函數返回值) 三個函數的數組,那麼 compose(...chain)(store.dispatch)
即爲 C1(C2(C3(store.dispatch)))
,並且:
applyMiddleware
的最後一箇中間件 M3 中的 next
就是原始的 store.dispatch
next
爲 C3(store.dispatch)
next
爲 C2(C3(store.dispatch))
最終將 C1(C2(C3(store.dispatch)))
做爲新的 dispatch
掛載在 store
中返回給用戶,做爲用戶實際調用的 dispatch
方法。因爲已經層層調用了 C3,C2,C1,中間件的結構已經從 next => action => {}
被拆解爲 acion => {}
咱們能夠梳理一遍當用戶觸發一個 action 的完整流程:
store.dispatch(action)
C1(C2(C3(store.dispatch)))(action)
next(action)
,此時的 next
爲 M1 中的 next
,即:C2(C3(store.dispatch))
C2(C3(store.dispatch))(action)
,直到遇到 next(action)
,此時的 next
爲 M2 中的 next
,即:C3(store.dispatch)
C3(store.dispatch)(action)
,直到遇到 next(action)
,此時的 next
爲 M3 中的 next
,即:store.dispatch
store.dispatch(action)
,store.dispatch(action)
內部調用 root reducer
更新當前 state
next(action)
以後的代碼next(action)
以後的代碼next(action)
以後的代碼其實這就是所謂的洋蔥模型,Koa 中的中間件執行機制也是如此。
對於上面的 applyMiddleware(logger, crashReporter)
,若是咱們執行
export const store = createStore( counter, applyMiddleware(logger, crashReporter) ); store.subscribe(() => console.log("store change", store.getState())); store.dispatch({ type: "INCREMENT" });
,結果將是
先觸發 logger,輸出 dispatching
,執行 next(action)
;而後在 crashReporter 中無異常,沒有輸出;執行 Reducer,獲得新的 state,store 中監聽到狀態變化,輸出 store change
;最後執行 logger 中 next
以後的語句
若是是 store.dispatch()
,由於 action 必須是一個對象,因此在 crashReporter 中將會捕獲異常,並拋出錯誤,結果爲:
【demo】
這個應該是最經常使用到的 Redux 中間件了,是咱們在 Redux 中處理異步請求的經常使用方案。redux-thunk 的實現很是簡單,只有14 行代碼(包括空行):
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;
其主要邏輯爲,檢查 action
的類型,若是是函數,就執行 action
函數,並把 dispatch
和 getState
做爲參數傳遞進去;不然就調用 next
讓下一個中間件繼續處理 action
。
因此,咱們能夠經過使用 redux-thunk 來在 action 生成器(action creator)中返回一個函數而不是簡單的 action 對象。從而實現 action 的異步 dispatch,如:
const INCREMENT_COUNTER = 'INCREMENT_COUNTER'; function increment() { return { type: INCREMENT_COUNTER, }; } function incrementAsync() { return (dispatch) => { setTimeout(() => { // Yay! Can invoke sync or async actions with `dispatch` dispatch(increment()); }, 1000); }; }
或在特定條件下才發送 action,如:
function incrementIfOdd() { return (dispatch, getState) => { const { counter } = getState(); if (counter % 2 === 0) { return; } dispatch(increment()); }; }