Middleware(中間件)本意是指位於服務器的操做系統之上,管理計算資源和網絡通訊的一種通用獨立的系統軟件服務程序。分佈式應用軟件藉助這種軟件在不一樣的技術之間共享資源。而在大前端領域,Middleware 的含義則簡單得多,通常指提供通用獨立功能的數據處理函數。典型的 Middleware 包括日誌記錄、數據疊加和錯誤處理等。本文將橫向對比大前端領域內各大框架的 Middleware 使用場景和實現原理,包括Express, Koa, Redux和Axios。前端
這裏說的大前端領域天然就包括了服務器端和客戶端了。最先提出 Middleware 概念的是Express, 隨後由原班人馬打造的Koa不但沿用了 Middleware 的架構設計,還更加完全的把本身定義爲中間件框架。node
Expressive HTTP middleware framework for node.js
在客戶端領域,Redux也引入了 Middleware 的概念,方便獨立功能的函數對 Action 進行處理。Axios雖然沒有中間件,但其攔截器的用法卻跟中間件十分類似,也順便拉進來一塊兒比較。下面的表格橫向比較了幾個框架的中間件或類中間件的使用方式。ios
框架 | use註冊 | next調度 | compose編排 | 處理對象 |
---|---|---|---|---|
Express | Y | Y | N | req & res |
Koa | Y | Y | Y | ctx |
Redux | N | Y | Y | action |
Axios | Y | N | N | config/data |
下面咱們一塊兒來拆解這些框架的內部實現方式。編程
app.use(function logMethod(req, res, next) { console.log('Request Type:', req.method) next() })
Express的 Middleware 有多種層級的註冊方式,在此以應用層級的中間件爲例子。這裏看到 2 個關鍵字,use和next。Express經過use註冊,next觸發下一中間件執行的方式,奠基了中間件架構的標準用法。axios
原理部分會對源碼作極端的精簡,只保留核心。數組
var stack = []; function use(fn) { stack.push(fn); }
function handle(req, res) { var idx = 0; next(); function next() { var fn = stack[idx++]; fn(req, res, next) } }
當請求到達的時候,會觸發handle方法。接着next函數從隊列中順序取出 Middleware 並執行。promise
app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); });
跟Express相比,Koa的 Middleware 註冊跟路由無關,全部的請求都會通過註冊的中間件。同時Koa與生俱來支持async/await異步編程模式,代碼風格更加簡潔。至於洋蔥模型什麼的你們都清楚,就不廢話了。前端框架
var middleware = []; function use(fn) { middleware.push(fn); }
function compose (middleware) { returnfunction (context, next) { let index = -1 return dispatch(0) function dispatch (i) { index = i let fn = middleware[i] // middleware執行完的後續操做,結合koa的源碼,這裏的next=undefined if (i === middleware.length) fn = next if (!fn) returnPromise.resolve() try { returnPromise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { returnPromise.reject(err) } } } }
跟Express相似,Koa的 Middleware 也是順序執行的,經過dispatch函數來控制。代碼的編寫模式也很像:調用dispatch/next -> 定義dispatch/next -> dispatch/next做爲回調遞歸調用。這裏有個地方要注意下,對於 Middleware 來講,它們的await next()實際上就是await dispatch(i)。當執行到最後一個 Middleware 的時候,會觸發條件if (i === middleware.length) fn = next,這裏的next是undefined,會觸發條if (!fn) return Promise.resolve(),繼續執行最後一個 Middleware await next()後面的代碼,也是洋蔥模型由內往外執行的時間點。服務器
Redux是我所知的第一個將 Middleware 概念應用到客戶端的前端框架,它的源碼到處體現出函數式編程的思想,讓人眼前一亮。網絡
const logger = store =>next =>action => { console.info('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) } } const store = createStore(appReducer, applyMiddleware(logger, crashReporter))
Redux中間件的參數作過柯里化,store是applyMiddleware內部傳進來的,next是compose後傳進來的,action是dispatch傳進來的。這裏的設計確實十分巧妙,下面咱們結合源碼來進行分析。
export default function applyMiddleware(...middlewares) { return(createStore) =>(reducer, preloadedState) => { const store = createStore(reducer, preloadedState) let dispatch = store.dispatch let chain = [] const middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) } // 先執行一遍middleware,把第一個參數store傳進去 chain = middlewares.map(middleware => middleware(middlewareAPI)) // 傳入原始的dispatch dispatch = compose(...chain)(store.dispatch) return { ...store, dispatch } } }
這裏compose的返回值又從新賦值給dispatch,說明咱們在應用內調用的dispatch並非store自帶的,而是通過 Middleware 處理的升級版。
function compose (...funcs) { if (funcs.length === 0) { returnarg => arg } if (funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) =>(...args) => a(b(...args))) }
compose的核心代碼只有一行,像套娃同樣的將 Middleware 一層一層的套起來,最底層的args就是store.dispatch。
Axios中沒有 Middleware 的概念,但卻有相似功能的攔截器(interceptors),本質上都是在數據處理鏈路的 2 點之間,提供獨立的、配置化的、可疊加的額外功能。
// 請求攔截器 axios.interceptors.request.use(function (config) { config.headers.token = 'added by interceptor'; return config; }); // 響應攔截器 axios.interceptors.response.use(function (data) { data.data = data.data + ' - modified by interceptor'; return data; });
Axios的 interceptors 分請求和響應 2 種,註冊後會自動按註冊的順序執行,無需像其餘框架同樣要手動調用next()。
function Axios(instanceConfig) { this.defaults = instanceConfig; this.interceptors = { request: new InterceptorManager(), response: new InterceptorManager() }; } function InterceptorManager() { this.handlers = []; } InterceptorManager.prototype.use = function use(fulfilled, rejected) { this.handlers.push({ fulfilled: fulfilled, rejected: rejected }); returnthis.handlers.length - 1; };
能夠看到Axios內部會維護 2 個 interceptors,它們有獨立的 handlers 數組。use就是往數組添加元素而已,跟其它框架不一樣的是這裏的數組元素不是一個函數,而是一個對象,包含fulfilled和rejected 2 個屬性。第二個參數不傳的時候rejected就是 undefined。
// 精簡後的代碼 Axios.prototype.request = function request(config) { config = mergeConfig(this.defaults, config); // 成對的添加元素 var requestInterceptorChain = []; this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected); }); var responseInterceptorChain = []; this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected); }); var chain = [dispatchRequest, undefined]; Array.prototype.unshift.apply(chain, requestInterceptorChain); chain.concat(responseInterceptorChain); promise = Promise.resolve(config); while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); } return promise; }
這裏經過 promise 的鏈式調用,將 interceptors 串聯了起來,執行順序是:requestInterceptorChain -> chain -> responseInterceptorChain。這裏有一個默認的約定,chain 裏的元素都是按照[fulfilled1, rejected1, fulfilled2, rejected2]這種模式排列的,因此註冊 interceptors 的時候若是沒有提供第二個參數,也會有一個默認值 undefined。
看了各大框架的 Middleware 實現方式以後,咱們能夠總結出如下幾個特色:
咱們再來總結一下各大框架中間件系統實現方式的精髓:
框架 | 實現方式 |
---|---|
Express | 遞歸調用next |
Koa | 遞歸調用dispatch |
Redux | Array.reduce 實現函數嵌套 |
Axios | promise.then 鏈式調用 |
這裏面最精妙也是最難理解的就是Array.reduce這種形式,須要反覆的推敲。promise.then鏈式調用的任務編排方法也十分巧妙,前面處理完的數據會自動傳給下一個then。遞歸調用的形式則最好理解,Koa在Express實現的基礎上自然支持異步調用,更符合服務器端場景。
本文從使用方式入手,結合源碼講解了各大前端框架中 Middleware 的實現方式,橫向對比了他們之間的異同。當中的遞歸調用、函數嵌套和 promise 鏈式調用的技巧很是值得咱們借鑑學習。