上一篇《nodejs之express中間件》已經對express中間件的實現作了詳細的講解,同時也對實現中間件的框架Connect的源碼作了簡單的分析。而且也提到了express的中間件是直線型,koa的中間件是洋蔥型。本篇就來講說koa的中間件。node
koa和express是同一個團隊開發的。與express很像,也是一個自身功能極簡的框架,因此在一個項目中所須要的東西大可能是以中間件的形式引入。git
目前koa有1.x和2.x版本,1.x版本基於generator,2.x版本基於
generator/async、await。因爲generator的語法相比async又很明顯的劣勢,因此後續的版本中會去掉generator的使用,而是所有采用async的方式。不過在2.x這個過渡版中依然兼容generator,須要注意的是在使用generator或者使用了依賴generator的第三方庫時,會報出一個警告,大體意思「generator在當前版本還能夠正常使用,可是會在後續的版本中移除」。
注:本文全部koa的寫法都是koa2版本的。而且兼容koa3。github
相比express的直線型中間件,koa的中間件就不是那麼直觀了。先看一張圖
把洋蔥的一圈看作是一箇中間件,直線型就是從第一個中間件走到最後一個,可是洋蔥型就很特殊了,最先use的中間件在洋蔥的最外層,開始的時候會按順序走到全部中間件,而後按照倒序再走一遍全部的中間件,至關於每一箇中間件都會進入兩次。這就給了咱們更多的操做空間。
看下面一段代碼express
const koa = require('koa'); let server = new koa(); server.use(async (ctx, next) => { console.log('a-1'); next(); console.log('a-2'); }) server.use(async (ctx, next) => { console.log('b-1'); next(); console.log('b-2'); }) server.use(async (ctx, next) => { console.log('c'); }) server.listen(3000);
代碼執行後命令行輸出順序爲 segmentfault
通常的中間件都會執行兩次,調用next以前爲第一次,調用next時把控制按順序傳遞給下游的中間件。當下遊再也不有中間件或者中間件沒有執行next函數時,就將依次恢復上游中間件的行爲,讓上游中間件執行next以後的代碼。數組
使用方式咱們已經搞清了,下面就看看內部實現原理閉包
在github上找到koa的源碼,核心的函數都在application.js中。
通過上一篇connect源碼的學習,這部分源碼讀起來應該也是壓力不大。所有代碼也就200多行,這裏我只截取其中比較重要的部分。app
'use strict'; module.exports = class Application extends Emitter { constructor() { super(); this.proxy = false; this.middleware = []; this.subdomainOffset = 2; this.env = process.env.NODE_ENV || 'development'; this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); if (util.inspect.custom) { this[util.inspect.custom] = this.inspect; } } listen(...args) { debug('listen'); const server = http.createServer(this.callback()); return server.listen(...args); } use(fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); if (isGeneratorFunction(fn)) { deprecate('Support for generators will be removed in v3. ' + 'See the documentation for examples of how to convert old middleware ' + 'https://github.com/koajs/koa/blob/master/docs/migration.md'); fn = convert(fn); } debug('use %s', fn._name || fn.name || '-'); this.middleware.push(fn); return this; } callback() { const fn = compose(this.middleware); if (!this.listenerCount('error')) this.on('error', this.onerror); const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest; } handleRequest(ctx, fnMiddleware) { const res = ctx.res; res.statusCode = 404; const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); onFinished(res, onerror); return fnMiddleware(ctx).then(handleResponse).catch(onerror); } };
1.先來重點看下use函數
第一個if是用來作校驗的,第二個if是用來兼容generator函數的。
use的最後執行了this.middleware.push(fn);
就是將中間件放到middleware數組中,相似connect中的stack數組。框架
2.再來看下callback函數callback
函數的返回值就是listen
函數中執行createServer
函數的回調,也就是handleRequest
函數,也能夠理解成是用來響應request
事件的函數。dom
3.注意在callback中執行this.handleRequest(ctx, fn);
的時候這個fn是中間件數組經過compose函數處理後返回的值。那在看handleRequest
函數中最後就調用了這個函數,那麼在這以前就要知道compose函數對中間件數組作了什麼?
找到koa-compose這個包,截取其中的compose函數。
function compose(middleware) { if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!') for (const fn of middleware) { if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!') } return function (context, next) { // last called middleware # let index = -1 return dispatch(0) function dispatch(i) { if (i <= index) return Promise.reject(new Error('next() called multiple times')) 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) } } } }
前面兩段很明顯是在驗證數組和函數的類型。而後就是返回一個函數,這個函數就是callback函數中的fn,也是在傳入handleRequest
函數中最後調用的那個函數。
先總結下上面的內容:
this.middleware
數組中。this.middleware
數組進行操做,將數組轉換爲一個函數賦值給fn(注意這個函數比較特殊,下面會詳細講解),而後再經過handleRequest
函數去調用這個fn函數,顯而易見這個函數必定返回Promise對象。這個流程若是搞清楚後,再來看看這個compose到底作了什麼。
實際上compose就是返回一個函數,函數內利用閉包實現了對中間件數組的遍歷,返回一個Promise對象,而且resolve回調就是執行當前索引對應的中間件函數,而且將參數next賦值爲內部閉包函數的調用,調用的同時將索引值+1,這樣在中間件中執行next()就至關於執行了dispatch(i+1),天然就找到了下一個中間件。若是中間件沒有執行next或者遍歷了數組中所有的中間件後,天然就開始一級一級向上執行每一箇中間件next後的代碼。這就是洋蔥圈的實現。
這裏的邏輯稍微有點饒,若是仍是不懂也不要緊,我將代碼流程簡化,經過圖片再來講下整個實現過程。
整個核心仍是compose函數。若是將這個函數簡化到極致,就是以下的代碼
function a(){ console.log('a-1'); next(b); console.log('a-2'); } function b(){ console.log('b-1'); next(c); console.log('b-2'); } function c(){ console.log('c'); } function next(fn){ return Promise.resolve(fn()); } // 輸出結果爲 // a-1 // b-1 // c // b-2 // a-2
到此咱們已經分析了express中間件和koa中間件的實現原理。兩者的源碼都是很是簡潔,而且設計巧妙、可讀性高。不少細節處理都值得借鑑。但願本身在源碼閱讀的道路上越走越遠。