最近經常使用koa進行服務端開發,迷戀上了koa的洋蔥模型,覺得這玩意太好用了。而且koa是以精簡爲主,沒有很多集成東西,所有的東西都需按需加載,這個更是太合我胃口了哈哈哈哈。
相對與express的中間件,express的中間件使用的是串聯,就像冰糖葫蘆一樣一個接着一個,而koa使用的V型結構(洋蔥模型),這將給我們的中間件提供更加靈活的處理方式。
基於對洋蔥模型的熱衷,所以對koa的洋蔥模型進行一探究竟,不管是koa1還是koa2的中間件都是基於koa-compose進行編寫的,這種V型結構的實現就來源於koa-compose。
附上源碼先:
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)
}
}
}
}
看到這裏,如果下面的那些能夠理解,那麼下面的可以不用看的,還是不能理解的就繼續往下看,詳細一點的分析。
- 首先,我們用app.use()添加一箇中間件,在koa的源碼裏app.use()這個方法就是將一箇中間件push進middleware這個中間件列表裏。源碼裏是這麼寫的(這個比較簡單 不做分析):
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()添加進去的方法列表,首先會判斷列表是否爲數組,中間件是否爲方法,如果不是就直接拋出類型錯誤。
- compose返回的是一個函數,這裏使用閉包來緩存中間件列表,然後這個函數接收兩個參數,第一個參數是context,是一個存放配置信息的對象。第二份參數是一個next方法,也是洋蔥模型的中心或者說是V型模型的拐點。
- 創建一個index變量來保存執行的中間件索引,然後從第一個中間件開始開始遞歸執行。
let index = -1
return dispatch(0)
- dispatch方法就是執行中間件,先判斷索引,如果i小於index那麼說明在同一個中間件裏執行了兩次或兩次以上的next函數,如果i>index則說明該中間件還未執行,那麼將該中間件的所以記錄下來
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
- 取出該中間件,如果i等於中間件的長圖,則說明執行到了洋蔥模型的中心,則最後一箇中間件,如果中間件爲空,那麼就直接resovle掉
let fn = middleware[i]
if(i === middleware.length){
fn = next
}
if(!fn){
return Promise.resolve()
}
- 到了最刺激的部分了,也是有點繞的部分了,首先爲啥return的是一個Promise的對象(Promise.resolve也是一個promise對象)呢,因爲我們await next()的時候,await是等待且執行一個async函數的完成,async會默認返回一個promise對象,所以這裏return的是一個promise對象。我們在每個中間裏面await mext() next()指的就是下一個中間件,也就是
fn(context, function next () {
return dispatch(i + 1)
})
所以我們上一個中的await 等待的就是dispatch(i+1)的執行完成,dispatch返回的是Promise.resolve(fn(context, function next () { xxxx })),這樣來看雖然一開始只執行了dispatch(0),但是是由這個函數形成了一條執行鏈。
以三個中間件執行爲例,dispatch(0)執行後就形成:
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一個個往上傳遞,這就形成了一個洋蔥模型。
最後附上測試代碼:
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一些源碼的分析。
前端小學生(應屆畢業生),如有錯誤或者其他想法的。歡迎指正交流~