淺談redux, koa, express 中間件實(shí)現(xiàn)對(duì)比解析
如果你有 express ,koa, redux 的使用經(jīng)驗(yàn),就會(huì)發(fā)現(xiàn)他們都有 中間件(middlewares)的概念,中間件 是一種攔截器的思想,用于在某個(gè)特定的輸入輸出之間添加一些額外處理,同時(shí)不影響原有操作。
最開(kāi)始接觸 中間件是在服務(wù)端使用 express 和 koa 的時(shí)候,后來(lái)從服務(wù)端延伸到前端,看到其在redux的設(shè)計(jì)中也得到的極大的發(fā)揮。中間件的設(shè)計(jì)思想也為許多框架帶來(lái)了靈活而強(qiáng)大的擴(kuò)展性。
本文主要對(duì)比redux, koa, express 的中間件實(shí)現(xiàn),為了更直觀,我會(huì)抽取出三者中間件相關(guān)的核心代碼,精簡(jiǎn)化,寫(xiě)出模擬示例。示例會(huì)保持 express, koa,redux 的整體結(jié)構(gòu),盡量保持和源碼一致,所以本文也會(huì)稍帶講解下express, koa, redux 的整體結(jié)構(gòu)和關(guān)鍵實(shí)現(xiàn):
示例源碼地址, 可以一邊看源碼,一邊讀文章,歡迎star!
本文適合對(duì)express ,koa ,redux 都有一定了解和使用經(jīng)驗(yàn)的開(kāi)發(fā)者閱讀
服務(wù)端的中間件
express 和 koa 的中間件是用于處理 http 請(qǐng)求和響應(yīng)的,但是二者的設(shè)計(jì)思路確不盡相同。大部分人了解的express和koa的中間件差異在于:
- express采用“尾遞歸”方式,中間件一個(gè)接一個(gè)的順序執(zhí)行, 習(xí)慣于將response響應(yīng)寫(xiě)在最后一個(gè)中間件中;
- 而koa的中間件支持 generator, 執(zhí)行順序是“洋蔥圈”模型。
所謂的“洋蔥圈”模型:
不過(guò)實(shí)際上,express 的中間件也可以形成“洋蔥圈”模型,在 next 調(diào)用后寫(xiě)的代碼同樣會(huì)執(zhí)行到,不過(guò)express中一般不會(huì)這么做,因?yàn)?express的response一般在最后一個(gè)中間件,那么其它中間件 next() 后的代碼已經(jīng)影響不到最終響應(yīng)結(jié)果了;
express
首先看一下 express 的實(shí)現(xiàn):
入口
// express.js var proto = require('./application'); var mixin = require('merge-descriptors'); exports = module.exports = createApplication; function createApplication() { // app 同時(shí)是一個(gè)方法,作為http.createServer的處理函數(shù) var app = function(req, res, next) { app.handle(req, res, next) } mixin(app, proto, false); return app }
這里其實(shí)很簡(jiǎn)單,就是一個(gè) createApplication 方法用于創(chuàng)建 express 實(shí)例,要注意返回值 app 既是實(shí)例對(duì)象,上面掛載了很多方法,同時(shí)它本身也是一個(gè)方法,作為 http.createServer的處理函數(shù), 具體代碼在 application.js 中:
// 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 調(diào)用 nodejs 的http.createServer 創(chuàng)建web服務(wù),可以看到這里 var server = http.createServer(this) 其中 this 即 app 本身, 然后真正的處理程序即 app.handle;
中間件處理
express 本質(zhì)上就是一個(gè)中間件管理器,當(dāng)進(jìn)入到 app.handle 的時(shí)候就是對(duì)中間件進(jìn)行執(zhí)行的時(shí)候,所以,最關(guān)鍵的兩個(gè)函數(shù)就是:
- app.handle 尾遞歸調(diào)用中間件處理 req 和 res
- app.use 添加中間件
全局維護(hù)一個(gè)stack數(shù)組用來(lái)存儲(chǔ)所有中間件,app.use 的實(shí)現(xiàn)就很簡(jiǎn)單了,可以就是一行代碼 ``
// app.use app.use = function(fn) { this.stack.push(fn) }
express 的真正實(shí)現(xiàn)當(dāng)然不會(huì)這么簡(jiǎn)單,它內(nèi)置實(shí)現(xiàn)了路由功能,其中有 router, route, layer 三個(gè)關(guān)鍵的類,有了 router 就要對(duì) path 進(jìn)行分流,stack 中保存的是 layer實(shí)例,app.use 方法實(shí)際調(diào)用的是 router 實(shí)例的 use 方法, 有興趣的可以自行去閱讀。
app.handle 即對(duì) stack 數(shù)組進(jìn)行處理
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() }
這里就是所謂的"尾遞歸調(diào)用",next 方法不斷的取出stack中的“中間件”函數(shù)進(jìn)行調(diào)用,同時(shí)把next 本身傳遞給“中間件”作為第三個(gè)參數(shù),每個(gè)中間件約定的固定形式為 (req, res, next) => {}, 這樣每個(gè)“中間件“函數(shù)中只要調(diào)用 next 方法即可傳遞調(diào)用下一個(gè)中間件。
之所以說(shuō)是”尾遞歸“是因?yàn)檫f歸函數(shù)的最后一條語(yǔ)句是調(diào)用函數(shù)本身,所以每一個(gè)中間件的最后一條語(yǔ)句需要是next()才能形成”尾遞歸“,否則就是普通遞歸,”尾遞歸“相對(duì)于普通”遞歸“的好處在于節(jié)省內(nèi)存空間,不會(huì)形成深度嵌套的函數(shù)調(diào)用棧。有興趣的可以閱讀下阮老師的尾調(diào)用優(yōu)化
至此,express 的中間件實(shí)現(xiàn)就完成了。
koa
不得不說(shuō),相比較 express 而言,koa 的整體設(shè)計(jì)和代碼實(shí)現(xiàn)顯得更高級(jí),更精煉;代碼基于ES6 實(shí)現(xiàn),支持generator(async await), 沒(méi)有內(nèi)置的路由實(shí)現(xiàn)和任何內(nèi)置中間件,context 的設(shè)計(jì)也很是巧妙。
整體
一共只有4個(gè)文件:
- application.js 入口文件,koa應(yīng)用實(shí)例的類
- context.js ctx 實(shí)例,代理了很多request和response的屬性和方法,作為全局對(duì)象傳遞
- request.js koa 對(duì)原生 req 對(duì)象的封裝
- response.js koa 對(duì)原生 res 對(duì)象的封裝
request.js 和 response.js 沒(méi)什么可說(shuō)的,任何 web 框架都會(huì)提供req和res 的封裝來(lái)簡(jiǎn)化處理。所以主要看一下 context.js 和 application.js的實(shí)現(xiàn)
// context.js /** * Response delegation. */ delegate(proto, 'res') .method('setHeader') /** * Request delegation. */ delegate(proto, 'req') .access('url') .setter('href') .getter('ip');
context 就是這類代碼,主要功能就是在做代理,使用了 delegate 庫(kù)。
簡(jiǎn)單說(shuō)一下這里代理的含義,比如delegate(proto, 'res').method('setHeader') 這條語(yǔ)句的作用就是:當(dāng)調(diào)用proto.setHeader時(shí),會(huì)調(diào)用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 很多問(wèn)題處理更加方便 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方法中創(chuàng)建 web 服務(wù), 沒(méi)有使用 express 那么繞的方式,const server = http.createServer(this.callback()); 用this.callback()生成 web 服務(wù)的處理程序
callback 函數(shù)返回handleRequest, 所以真正的處理程序是this.handleRequest(ctx, fn)
中間件處理
構(gòu)造函數(shù) constructor 中維護(hù)全局中間件數(shù)組 this.middleware和全局的this.context 實(shí)例(源碼中還有request,response對(duì)象和一些其他輔助屬性)。和 express 不同,因?yàn)闆](méi)有router的實(shí)現(xiàn),所有this.middleware 中就是普通的”中間件“函數(shù)而非復(fù)雜的 layer 實(shí)例,
this.handleRequest(ctx, fn); 中 ctx 為第一個(gè)參數(shù),fn = compose(this.middleware) 作為第二個(gè)參數(shù), handleRequest 會(huì)調(diào)用 fnMiddleware(ctx).then(handleResponse).catch(onerror); 所以中間處理的關(guān)鍵在compose方法, 它是一個(gè)獨(dú)立的包koa-compose, 把它拿了出來(lái)看一下里面的內(nèi)容:
// 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 是不是很像,只不過(guò)他是promise形式的,因?yàn)橐С之惒剑岳斫馄饋?lái)就稍微麻煩點(diǎn):每個(gè)中間件是一個(gè)async (ctx, next) => {}, 執(zhí)行后返回的是一個(gè)promise, 第二個(gè)參數(shù) next的值為 dispatch.bind(null, i + 1) , 用于傳遞”中間件“的執(zhí)行,一個(gè)個(gè)中間件向里執(zhí)行,直到最后一個(gè)中間件執(zhí)行完,resolve 掉,它前一個(gè)”中間件“接著執(zhí)行 await next() 后的代碼,然后 resolve 掉,在不斷向前直到第一個(gè)”中間件“ resolve掉,最終使得最外層的promise resolve掉。
這里和express很不同的一點(diǎn)就是koa的響應(yīng)的處理并不在"中間件"中,而是在中間件執(zhí)行完返回的promise resolve后:
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
通過(guò) handleResponse 最后對(duì)響應(yīng)做處理,”中間件“會(huì)設(shè)置ctx.body, handleResponse也會(huì)主要處理 ctx.body ,所以 koa 的”洋蔥圈“模型才會(huì)成立,await next()后的代碼也會(huì)影響到最后的響應(yīng)。
至此,koa的中間件實(shí)現(xiàn)就完成了。
redux
不得不說(shuō),redux 的設(shè)計(jì)思想和源碼實(shí)現(xiàn)真的是漂亮,整體代碼量不多,網(wǎng)上已經(jīng)隨處可見(jiàn)redux的源碼解析,我就不細(xì)說(shuō)了。不過(guò)還是要推薦一波官網(wǎng)對(duì)中間件部分的敘述 : redux-middleware
這是我讀過(guò)的最好的說(shuō)明文檔,沒(méi)有之一,它清晰的說(shuō)明了 redux middleware 的演化過(guò)程,漂亮地演繹了一場(chǎng)從分析問(wèn)題到解決問(wèn)題,并不斷優(yōu)化的思維過(guò)程。
總體
本文還是主要看一下它的中間件實(shí)現(xiàn), 先簡(jiǎn)單說(shuō)一下 redux 的核心處理邏輯, createStore 是其入口程序,工廠方法,返回一個(gè) store 實(shí)例,store實(shí)例的最關(guān)鍵的方法就是 dispatch , 而 dispatch 要做的就是一件事:
currentState = currentReducer(currentState, action)
即調(diào)用reducer, 傳入當(dāng)前state和action返回新的state。
所以要模擬基本的 redux 執(zhí)行只要實(shí)現(xiàn) createStore , dispatch 方法即可。其它的內(nèi)容如 bindActionCreators, combineReducers 以及 subscribe 監(jiān)聽(tīng)都是輔助使用的功能,可以暫時(shí)不關(guān)注。
中間件處理
然后就到了核心的”中間件" 實(shí)現(xiàn)部分即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 中間件提供的擴(kuò)展是在 action 發(fā)起之后,到達(dá) reducer 之前,它的實(shí)現(xiàn)思路就和express 、 koa 有些不同了,它沒(méi)有通過(guò)封裝 store.dispatch, 在它前面添加 中間件處理程序,而是通過(guò)遞歸覆寫(xiě) dispatch ,不斷的傳遞上一個(gè)覆寫(xiě)的 dispatch 來(lái)實(shí)現(xiàn)。
每一個(gè) redux 中間件的形式為 store => next => action => { xxx }
這里主要有兩層函數(shù)嵌套:
最外層函數(shù)接收參數(shù)store, 對(duì)應(yīng)于 applyMiddleware.js 中的處理代碼是 const chain = middlewares.map(middleware => middleware(middlewareAPI)), middlewareAPI 即為傳入的store 。這一層是為了把 store 的 api 傳遞給中間件使用,主要就是兩個(gè)api:
- getState, 直接傳遞store.getState.
- dispatch: (...args) => dispatch(...args),這里的實(shí)現(xiàn)就很巧妙了,并不是store.dispatch, 而是一個(gè)外部的變量dispatch, 這個(gè)變量最終指向的是覆寫(xiě)后的dispatch, 這樣做的原因在于,對(duì)于 redux-thunk 這樣的異步中間件,內(nèi)部調(diào)用store.dispatch 的時(shí)候仍然后走一遍所有“中間件”。
返回的chain就是第二層的數(shù)組,數(shù)組的每個(gè)元素都是這樣一個(gè)函數(shù)next => action => { xxx }, 這個(gè)函數(shù)可以理解為 接受一個(gè)dispatch返回一個(gè)dispatch, 接受的dispatch 是后一個(gè)中間件返回的dispatch.
還有一個(gè)關(guān)鍵函數(shù)即 compose, 主要作用是 compose(f, g, h) 返回 () => f(g(h(..args)))
現(xiàn)在在來(lái)理解 dispatch = compose(...chain)(store.dispatch) 就相對(duì)容易了,原生的 store.dispatch 傳入最后一個(gè)“中間件”,返回一個(gè)新的 dispatch , 再向外傳遞到前一個(gè)中間件,直至返回最終的 dispatch, 當(dāng)覆寫(xiě)后的dispatch 調(diào)用時(shí),每個(gè)“中間件“的執(zhí)行又是從外向內(nèi)的”洋蔥圈“模型。
至此,redux中間件就完成了。
其它關(guān)鍵點(diǎn)
redux 中間件的實(shí)現(xiàn)中還有一點(diǎn)實(shí)現(xiàn)也值得學(xué)習(xí),為了讓”中間件“只能應(yīng)用一次,applyMiddleware 并不是作用在 store 實(shí)例上,而是作用在 createStore 工廠方法上。怎么理解呢?如果applyMiddleware 是這樣的
(store, middlewares) => {}
那么當(dāng)多次調(diào)用 applyMiddleware(store, middlewares) 的時(shí)候會(huì)給同一個(gè)實(shí)例重復(fù)添加同樣的中間件。所以 applyMiddleware 的形式是
(...middlewares) => (createStore) => createStore,
這樣,每一次應(yīng)用中間件時(shí)都是創(chuàng)建一個(gè)新的實(shí)例,避免了中間件重復(fù)應(yīng)用問(wèn)題。
這種形式會(huì)接收 middlewares 返回一個(gè) createStore 的高階方法,這個(gè)方法一般被稱為 createStore的 enhance 方法,內(nèi)部即增加了對(duì)中間件的應(yīng)用,你會(huì)發(fā)現(xiàn)這個(gè)方法和中間件第二層 (dispatch) => dispatch 的形式一致,所以它也可以用于compose 進(jìn)行多次增強(qiáng)。同時(shí)createStore 也有第三個(gè)參數(shù)enhance 用于內(nèi)部判斷,自增強(qiáng)。所以 redux 的中間件使用可以有兩種寫(xiě)法:
第一種:用 applyMiddleware 返回 enhance 增強(qiáng) createStore
store = applyMiddleware(middleware1, middleware2)(createStore)(reducer, initState)
第二種: createStore 接收一個(gè) enhancer 參數(shù)用于自增強(qiáng)
store = createStore(reducer, initState, applyMiddleware(middleware1, middleware2))
第二種使用會(huì)顯得直觀點(diǎn),可讀性更好。
縱觀 redux 的實(shí)現(xiàn),函數(shù)式編程體現(xiàn)的淋漓盡致,中間件形式 store => next => action => { xx } 是函數(shù)柯里化作用的靈活體現(xiàn),將多參數(shù)化為單參數(shù),可以用于提前固定 store 參數(shù),得到形式更加明確的 dispatch => dispatch,使得 compose得以發(fā)揮作用。
總結(jié)
總體而言,express 和 koa 的實(shí)現(xiàn)很類似,都是next 方法傳遞進(jìn)行遞歸調(diào)用,只不過(guò) koa 是promise 形式。redux 相較前兩者有些許不同,先通過(guò)遞歸向外覆寫(xiě),形成執(zhí)行時(shí)遞歸向里調(diào)用。
總結(jié)一下三者關(guān)鍵異同點(diǎn)(不僅限于中間件):
- 實(shí)例創(chuàng)建: express 使用工廠方法, koa是類
- koa 實(shí)現(xiàn)的語(yǔ)法更高級(jí),使用ES6,支持generator(async await)
- koa 沒(méi)有內(nèi)置router, 增加了 ctx 全局對(duì)象,整體代碼更簡(jiǎn)潔,使用更方便。
- koa 中間件的遞歸為 promise形式,express 使用while 循環(huán)加 next 尾遞歸
- 我更喜歡 redux 的實(shí)現(xiàn),柯里化中間件形式,更簡(jiǎn)潔靈活,函數(shù)式編程體現(xiàn)的更明顯
- redux 以 dispatch 覆寫(xiě)的方式進(jìn)行中間件增強(qiáng)
最后再次附上 模擬示例源碼 以供學(xué)習(xí)參考,喜歡的歡迎star, fork!
回答一個(gè)問(wèn)題
有人說(shuō),express 中也可以用 async function 作為中間件用于異步處理? 其實(shí)是不可以的,因?yàn)?express 的中間件執(zhí)行是同步的 while 循環(huán),當(dāng)中間件中同時(shí)包含 普通函數(shù) 和 async 函數(shù) 時(shí),執(zhí)行順序會(huì)打亂,先看這樣一個(gè)例子:
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'
在普通函數(shù)中直接調(diào)用async函數(shù), async 函數(shù)會(huì)同步執(zhí)行到第一個(gè) await 后的代碼,然后就立即返回一個(gè)promise, 等到內(nèi)部所有 await 的異步完成,整個(gè)async函數(shù)執(zhí)行完,promise 才會(huì)resolve掉.
所以,通過(guò)上述分析 express中間件實(shí)現(xiàn), 如果用async函數(shù)做中間件,內(nèi)部用await做異步處理,那么后面的中間件會(huì)先執(zhí)行,等到 await 后再次調(diào)用 next 索引就會(huì)超出!,大家可以自己在這里 express async 打開(kāi)注釋,自己嘗試一下。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
小程序?qū)崿F(xiàn)Token生成與驗(yàn)證
本文主要介紹了小程序?qū)崿F(xiàn)Token生成與驗(yàn)證,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12KnockoutJS 3.X API 第四章之?dāng)?shù)據(jù)控制流foreach綁定
這篇文章主要介紹了KnockoutJS 3.X API 第四章之?dāng)?shù)據(jù)控制流foreach綁定的相關(guān)資料,需要的朋友可以參考下2016-10-10JavaScript動(dòng)態(tài)創(chuàng)建二維數(shù)組的方法示例
這篇文章主要介紹了JavaScript動(dòng)態(tài)創(chuàng)建二維數(shù)組的方法,結(jié)合實(shí)例形式分析了javascript動(dòng)態(tài)創(chuàng)建二維數(shù)組的相關(guān)操作技巧與注意事項(xiàng),需要的朋友可以參考下2019-02-02jQuery 實(shí)現(xiàn)倒計(jì)時(shí)天,時(shí),分,秒功能
本文通過(guò)html代碼和js代碼給大家介紹了實(shí)現(xiàn)倒計(jì)時(shí)天,時(shí),分,秒功能,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2018-07-07微信小程序倒計(jì)時(shí)功能實(shí)現(xiàn)代碼
倒計(jì)時(shí)功能在項(xiàng)目開(kāi)發(fā)中經(jīng)常會(huì)用到,今天小編給大家介紹下微信小程序倒計(jì)時(shí)功能實(shí)現(xiàn)代碼,需要的朋友參考下吧2017-11-11javascript循環(huán)變量注冊(cè)dom事件 之強(qiáng)大的閉包
是在循環(huán)過(guò)程過(guò)this被改變,注冊(cè)過(guò)的事件也被隨之改變,找到了一種解決方法2010-09-09