最近常常使用koa進行服務端開發,迷戀上了koa的洋蔥模型,以爲這玩意太好用了。並且koa是以精簡爲主,沒有不少集成東西,全部的東西都需按需加載,這個更是太合我胃口了哈哈哈哈。javascript
相對與express的中間件,express的中間件使用的是串聯,就像冰糖葫蘆同樣一個接着一個,而koa使用的V型結構(洋蔥模型),這將給咱們的中間件提供更加靈活的處理方式。前端
基於對洋蔥模型的熱衷,因此對koa的洋蔥模型進行一探究竟,無論是koa1仍是koa2的中間件都是基於koa-compose進行編寫的,這種V型結構的實現就來源於koa-compose。
附上源碼先:java
function compose (middleware) { // 參數middleware 是一箇中間件數組,存放咱們用app.use()一個個串聯起來的中間件 // 判斷中間件列表是否爲數組,若是不爲數組,則拋出類型錯誤 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!') } /** 1. @param {Object} context 2. @return {Promise} 3. @api public */ return function (context, next) { // 這裏next指的是洋蔥模型的中心函數 // context是一個配置對象,保存着一些配置,固然也能夠利用context將一些參數往下一個中間傳遞 // last called middleware # let index = -1 // index是記錄執行的中間件的索引 return dispatch(0) // 執行第一個中間件 而後經過第一個中間件遞歸調用下一個中間件 function dispatch (i) { // 這裏是保證同個中間件中一個next()不被調用屢次調用 // 當next()函數被調用兩次的時候,i會小於index,而後拋出錯誤 if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i let fn = middleware[i] // 取出要執行的中間件 if (i === middleware.length) fn = next // 若是i 等於 中間件的長度,即到了洋蔥模型的中心(最後一箇中間件) if (!fn) return Promise.resolve() // 若是中間件爲空,即直接resolve try { // 遞歸執行下一個中間件 (下面會重點分析這個) return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); } catch (err) { return Promise.reject(err) } } } }
看到這裏,若是下面的那些可以理解,那麼下面的能夠不用看的,仍是不能理解的就繼續往下看,詳細一點的分析。git
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; }
compose這個方法傳入一箇中間件列表middleware,這個列表就是咱們使用use()添加進去的方法列表,首先會判斷列表是否爲數組,中間件是否爲方法,若是不是就直接拋出類型錯誤。github
let index = -1 return dispatch(0)
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() }
fn(context, function next () { return dispatch(i + 1) })
因此咱們上一個中的await 等待的就是dispatch(i+1)的執行完成,dispatch返回的是Promise.resolve(fn(context, function next () { xxxx })),這樣來看雖然一開始只執行了dispatch(0),可是是由這個函數造成了一條執行鏈。
以三個中間件執行爲例,dispatch(0)執行後就造成:express
Promise.resolve( // 第一個中間件 function(context,next){ // 這裏的next第二個中間件也就是dispatch(1) // await next上的代碼 (中間件1) await Promise.resolve( // 第二個中間件 function(context,next){ // 這裏的next第二個中間件也就是dispatch(2) // await next上的代碼 (中間件2) await Promise.resolve( // 第三個中間件 function(context,next){ // 這裏的next第二個中間件也就是dispatch(3) // await next上的代碼 (中間件3) await Promise.resolve() // await next下的代碼 (中間件3) } ) // await next下的代碼 (中間件2) } ) // await next下的代碼 (中間件2) } )
先執行await上面的代碼,而後等待最後一箇中間件resolve一個個往上傳遞,這就造成了一個洋蔥模型。
最後附上測試代碼:api
async function test1(ctx, next) { console.log('中間件1上'); await next(); console.log('中間件1下'); }; async function test2(ctx, next) { console.log('中間件2上'); await next(); console.log('中間件2下'); }; async function test3(ctx, next) { console.log('中間件3上'); await next(); console.log('中間件3下'); }; let middleware = [test1, test2, test3]; let cp = compose(middleware); cp('ctx', function() { console.log('中心'); });
OK,到這裏koa2的中間件核心(koa-compose)就解析完成了,一開始看的時候,也被繞了很久,多看幾遍多分析一步一步捋順。koa1的中間件等過幾天有時間再補上吧,koa1是基於generator,源碼比起koa2相對簡單。數組
最近在看koa2源碼,等有時間再繼續更新koa一些源碼的分析。promise
前端小學生(應屆畢業生),若有錯誤或者其餘想法的。歡迎指正交流~緩存