若是你有 express
,koa
, redux
的使用經驗,就會發現他們都有 中間件(middlewares)
的概念,中間件
是一種攔截器的思想,用於在某個特定的輸入輸出之間添加一些額外處理,同時不影響原有操做。html
最開始接觸 中間件
是在服務端使用 express
和 koa
的時候,後來從服務端延伸到前端,看到其在redux
的設計中也獲得的極大的發揮。中間件
的設計思想也爲許多框架帶來了靈活而強大的擴展性。前端
本文主要對比redux
, koa
, express
的中間件實現,爲了更直觀,我會抽取出三者中間件
相關的核心代碼,精簡化,寫出模擬示例。示例會保持 express
, koa
,redux
的總體結構,儘可能保持和源碼一致,因此本文也會稍帶講解下express
, koa
, redux
的總體結構和關鍵實現:node
示例源碼地址, 能夠一邊看源碼,一邊讀文章,歡迎star!git
本文適合對express ,koa ,redux 都有必定了解和使用經驗的開發者閱讀github
express
和 koa
的中間件是用於處理 http
請求和響應的,可是兩者的設計思路確不盡相同。大部分人瞭解的express
和koa
的中間件差別在於:web
express
採用「尾遞歸」方式,中間件一個接一個的順序執行, 習慣於將response
響應寫在最後一箇中間件中;koa
的中間件支持 generator
, 執行順序是「洋蔥圈」模型。所謂的「洋蔥圈」模型:express
不過實際上,express
的中間件也能夠造成「洋蔥圈」模型,在 next
調用後寫的代碼一樣會執行到,不過express
中通常不會這麼作,由於 express
的response
通常在最後一箇中間件,那麼其它中間件 next()
後的代碼已經影響不到最終響應結果了;編程
首先看一下 express 的實現:redux
// express.js
var proto = require('./application');
var mixin = require('merge-descriptors');
exports = module.exports = createApplication;
function createApplication() {
// app 同時是一個方法,做爲http.createServer的處理函數
var app = function(req, res, next) {
app.handle(req, res, next)
}
mixin(app, proto, false);
return app
}
複製代碼
這裏其實很簡單,就是一個 createApplication
方法用於建立 express
實例,要注意返回值 app
既是實例對象,上面掛載了不少方法,同時它自己也是一個方法,做爲 http.createServer
的處理函數, 具體代碼在 application.js 中:api
// application.js
var http = require('http');
var flatten = require('array-flatten');
var app = exports = module.exports = {}
app.listen = function listen() {
var server = http.createServer(this)
return server.listen.apply(server, arguments)
}
複製代碼
這裏 app.listen
調用 nodejs
的http.createServer
建立web
服務,能夠看到這裏 var server = http.createServer(this)
其中 this
即 app
自己, 而後真正的處理程序即 app.handle
;
express
本質上就是一箇中間件管理器,當進入到 app.handle
的時候就是對中間件進行執行的時候,因此,最關鍵的兩個函數就是:
全局維護一個stack
數組用來存儲全部中間件,app.use
的實現就很簡單了,能夠就是一行代碼 ``
// app.use
app.use = function(fn) {
this.stack.push(fn)
}
複製代碼
express
的真正實現固然不會這麼簡單,它內置實現了路由功能,其中有 router
, route
, layer
三個關鍵的類,有了 router
就要對 path
進行分流,stack
中保存的是 layer
實例,app.use
方法實際調用的是 router
實例的 use
方法, 有興趣的能夠自行去閱讀。
app.handle
即對 stack
數組進行處理
app.handle = function(req, res, callback) {
var stack = this.stack;
var idx = 0;
function next(err) {
if (idx >= stack.length) {
callback('err')
return;
}
var mid;
while(idx < stack.length) {
mid = stack[idx++];
mid(req, res, next);
}
}
next()
}
複製代碼
這裏就是所謂的"尾遞歸調用",next
方法不斷的取出stack
中的「中間件」函數進行調用,同時把next
自己傳遞給「中間件」做爲第三個參數,每一箇中間件約定的固定形式爲 (req, res, next) => {}
, 這樣每一個「中間件「函數中只要調用 next
方法便可傳遞調用下一個中間件。
之因此說是」尾遞歸「是由於遞歸函數的最後一條語句是調用函數自己,因此每個中間件的最後一條語句須要是next()
才能造成」尾遞歸「,不然就是普通遞歸,」尾遞歸「相對於普通」遞歸「的好處在於節省內存空間,不會造成深度嵌套的函數調用棧。有興趣的能夠閱讀下阮老師的尾調用優化
至此,express
的中間件實現就完成了。
不得不說,相比較 express
而言,koa
的總體設計和代碼實現顯得更高級,更精煉;代碼基於ES6
實現,支持generator(async await)
, 沒有內置的路由實現和任何內置中間件,context
的設計也非常巧妙。
一共只有4個文件:
ctx
實例,代理了不少request
和response
的屬性和方法,做爲全局對象傳遞koa
對原生 req
對象的封裝koa
對原生 res
對象的封裝request.js
和 response.js
沒什麼可說的,任何 web 框架都會提供req
和res
的封裝來簡化處理。因此主要看一下 context.js
和 application.js
的實現
// context.js
/**
* Response delegation.
*/
delegate(proto, 'res')
.method('setHeader')
/**
* Request delegation.
*/
delegate(proto, 'req')
.access('url')
.setter('href')
.getter('ip');
複製代碼
context
就是這類代碼,主要功能就是在作代理,使用了 delegate
庫。
簡單說一下這裏代理的含義,好比delegate(proto, 'res').method('setHeader')
這條語句的做用就是:當調用proto.setHeader時,會調用proto.res.setHeader 即,將proto
的 setHeader
方法代理到proto
的res
屬性上,其它相似。
// application.js 中部分代碼
constructor() {
super()
this.middleware = []
this.context = Object.create(context)
}
use(fn) {
this.middleware.push(fn)
}
listen(...args) {
debug('listen')
const server = http.createServer(this.callback());
return server.listen(...args);
}
callback() {
// 這裏即中間件處理代碼
const fn = compose(this.middleware);
const handleRequest = (req, res) => {
// ctx 是koa的精髓之一, req, res上的不少方法代理到了ctx上, 基於 ctx 不少問題處理更加方便
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
ctx.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
複製代碼
一樣的在listen
方法中建立 web
服務, 沒有使用 express
那麼繞的方式,const server = http.createServer(this.callback());
用this.callback()
生成 web
服務的處理程序
callback
函數返回handleRequest
, 因此真正的處理程序是this.handleRequest(ctx, fn)
構造函數 constructor
中維護全局中間件數組 this.middleware
和全局的this.context
實例(源碼中還有request,response對象和一些其餘輔助屬性)。和 express
不一樣,由於沒有router
的實現,全部this.middleware
中就是普通的」中間件「函數而非複雜的 layer
實例,
this.handleRequest(ctx, fn);
中 ctx
爲第一個參數,fn = compose(this.middleware)
做爲第二個參數, handleRequest
會調用 fnMiddleware(ctx).then(handleResponse).catch(onerror);
因此中間處理的關鍵在compose
方法, 它是一個獨立的包koa-compose
, 把它拿了出來看一下里面的內容:
// compose.js
'use strict'
module.exports = compose
function compose (middleware) {
return function (context, next) {
let index = -1
return dispatch(0)
function dispatch (i) {
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
複製代碼
和express
中的next
是否是很像,只不過他是promise
形式的,由於要支持異步,因此理解起來就稍微麻煩點:每一個中間件
是一個async (ctx, next) => {}
, 執行後返回的是一個promise
, 第二個參數 next
的值爲 dispatch.bind(null, i + 1)
, 用於傳遞」中間件「的執行,一個個中間件向裏執行,直到最後一箇中間件執行完,resolve
掉,它前一個」中間件「接着執行 await next()
後的代碼,而後 resolve
掉,在不斷向前直到第一個」中間件「 resolve
掉,最終使得最外層的promise
resolve
掉。
這裏和
express
很不一樣的一點就是koa
的響應的處理並不在"中間件"中,而是在中間件執行完返回的promise
resolve
後:
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
經過
handleResponse
最後對響應作處理,」中間件「會設置ctx.body
,handleResponse
也會主要處理ctx.body
,因此koa
的」洋蔥圈「模型纔會成立,await next()
後的代碼也會影響到最後的響應。
至此,koa的中間件實現就完成了。
不得不說,redux
的設計思想和源碼實現真的是漂亮,總體代碼量很少,網上已經隨處可見redux
的源碼解析,我就不細說了。不過仍是要推薦一波官網對中間件部分的敘述 : redux-middleware
這是我讀過的最好的說明文檔,沒有之一,它清晰的說明了 redux middleware
的演化過程,漂亮地演繹了一場從分析問題
到解決問題
,並不斷優化的思惟過程。
本文仍是主要看一下它的中間件實現, 先簡單說一下 redux
的核心處理邏輯, createStore 是其入口程序,工廠方法,返回一個 store
實例,store
實例的最關鍵的方法就是 dispatch , 而 dispatch
要作的就是一件事:
currentState = currentReducer(currentState, action)
即調用reducer
, 傳入當前state
和action
返回新的state
。
因此要模擬基本的 redux
執行只要實現 createStore
, dispatch
方法便可。其它的內容如 bindActionCreators
, combineReducers
以及 subscribe
監聽都是輔助使用的功能,能夠暫時不關注。
而後就到了核心的」中間件" 實現部分即 applyMiddleware.js:
// applyMiddleware.js
import compose from './compose'
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)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
複製代碼
redux
中間件提供的擴展是在 action
發起以後,到達 reducer
以前,它的實現思路就和express
、 koa
有些不一樣了,它沒有經過封裝 store.dispatch
, 在它前面添加 中間件處理程序
,而是經過遞歸覆寫 dispatch
,不斷的傳遞上一個覆寫的 dispatch
來實現。
每個 redux
中間件的形式爲 store => next => action => { xxx }
這裏主要有兩層函數嵌套:
最外層函數接收參數store
, 對應於 applyMiddleware.js
中的處理代碼是 const chain = middlewares.map(middleware => middleware(middlewareAPI))
, middlewareAPI
即爲傳入的store
。這一層是爲了把 store
的 api
傳遞給中間件使用,主要就是兩個api
:
getState
, 直接傳遞store.getState
.dispatch: (...args) => dispatch(...args)
,這裏的實現就很巧妙了,並非store.dispatch
, 而是一個外部的變量dispatch
, 這個變量最終指向的是覆寫後的dispatch
, 這樣作的緣由在於,對於 redux-thunk
這樣的異步中間件,內部調用store.dispatch
的時候仍而後走一遍全部「中間件」。返回的chain
就是第二層的數組,數組的每一個元素都是這樣一個函數next => action => { xxx }
, 這個函數能夠理解爲 接受一個dispatch
返回一個dispatch
, 接受的dispatch
是後一箇中間件返回的dispatch
.
還有一個關鍵函數即 compose, 主要做用是 compose(f, g, h)
返回 () => f(g(h(..args)))
如今在來理解 dispatch = compose(...chain)(store.dispatch)
就相對容易了,原生的 store.dispatch
傳入最後一個「中間件」,返回一個新的 dispatch
, 再向外傳遞到前一箇中間件,直至返回最終的 dispatch
, 當覆寫後的dispatch
調用時,每一個「中間件「的執行又是從外向內的」洋蔥圈「模型。
至此,redux中間件就完成了。
redux
中間件的實現中還有一點實現也值得學習,爲了讓」中間件「只能應用一次,applyMiddleware
並非做用在 store
實例上,而是做用在 createStore
工廠方法上。怎麼理解呢?若是applyMiddleware
是這樣的
(store, middlewares) => {}
那麼當屢次調用 applyMiddleware(store, middlewares)
的時候會給同一個實例重複添加一樣的中間件。因此 applyMiddleware
的形式是
(...middlewares) => (createStore) => createStore
,
這樣,每一次應用中間件時都是建立一個新的實例,避免了中間件重複應用問題。
這種形式會接收 middlewares
返回一個 createStore
的高階方法,這個方法通常被稱爲 createStore
的 enhance
方法,內部即增長了對中間件的應用,你會發現這個方法和中間件第二層 (dispatch) => dispatch
的形式一致,因此它也能夠用於compose
進行屢次加強。同時createStore
也有第三個參數enhance
用於內部判斷,自加強。因此 redux
的中間件使用能夠有兩種寫法:
第一種:用 applyMiddleware 返回 enhance 加強 createStore
store = applyMiddleware(middleware1, middleware2)(createStore)(reducer, initState)
複製代碼
第二種: createStore 接收一個 enhancer 參數用於自加強
store = createStore(reducer, initState, applyMiddleware(middleware1, middleware2))
複製代碼
第二種使用會顯得直觀點,可讀性更好。
縱觀 redux
的實現,函數式編程體現的淋漓盡致,中間件形式 store => next => action => { xx }
是函數柯里化做用的靈活體現,將多參數化爲單參數,能夠用於提早固定 store
參數,獲得形式更加明確的 dispatch => dispatch
,使得 compose
得以發揮做用。
整體而言,express
和 koa
的實現很相似,都是next
方法傳遞進行遞歸調用,只不過 koa
是promise
形式。redux
相較前二者有些許不一樣,先經過遞歸向外覆寫,造成執行時遞歸向裏調用。
總結一下三者關鍵異同點(不只限於中間件):
express
使用工廠方法, koa
是類koa
實現的語法更高級,使用ES6
,支持generator(async await)
koa
沒有內置router
, 增長了 ctx
全局對象,總體代碼更簡潔,使用更方便。koa
中間件的遞歸爲 promise
形式,express
使用while
循環加 next
尾遞歸redux
的實現,柯里化中間件形式,更簡潔靈活,函數式編程體現的更明顯redux
以 dispatch
覆寫的方式進行中間件加強最後再次附上 模擬示例源碼 以供學習參考,喜歡的歡迎star, fork!
有人說,express
中也能夠用 async function
做爲中間件用於異步處理? 實際上是不能夠的,由於 express
的中間件執行是同步的 while
循環,當中間件中同時包含 普通函數
和 async 函數
時,執行順序會打亂,先看這樣一個例子:
function a() {
console.log('a')
}
async function b() {
console.log('b')
await 1
console.log('c')
await 2
console.log('d')
}
function f() {
a()
b()
console.log('f')
}
複製代碼
這裏的輸出是 'a' > 'b' > 'f' > 'c'
在普通函數中直接調用async
函數, async
函數會同步執行到第一個 await
後的代碼,而後就當即返回一個promise
, 等到內部全部 await
的異步完成,整個async
函數執行完,promise
纔會resolve
掉.
因此,經過上述分析 express
中間件實現, 若是用async
函數作中間件,內部用await
作異步處理,那麼後面的中間件會先執行,等到 await
後再次調用 next
索引就會超出!,你們能夠本身在這裏 express async 打開註釋,本身嘗試一下。