Koa框架實踐與中間件原理剖析
最近嘗試用了一下Koa,並在此記錄一下使用心得。
注意:本文是以讀者已經了解Generator和Promise為前提在寫的,因為單單Generator和Promise都能夠寫一篇博文來講解介紹了,所以就不在這里贅述。網上資料很多,可以自行查閱。
Koa是Express原班人馬打造的一個更小,基於nodejs平台的下一代web開發框架。Koa的精妙之處就在於其使用generator和promise,實現了一種更為有趣的中間件系統,Koa的中間件是一系列generator函數的對象,執行起來有點類似於棧的結構,依次執行。同時也類似於Python的django框架的中間件系統,以前蘇千大神做分享的時候把這種模型稱作為洋蔥模型。如圖:

當一個請求過來的時候,會依次經過各個中間件進行處理,中間件跳轉的信號是yield next,當到某個中間件后,該中間件處理完不執行yield next的時候,然后就會逆序執行前面那些中間件剩下的邏輯。直接上個官網的例子:
var koa = require('koa');
var app = koa();
// response-time中間件
app.use(function *(next){
var start = new Date;
yield next;
var ms = new Date - start;
this.set('X-Response-Time', ms + 'ms');
});
// logger中間件
app.use(function *(next){
var start = new Date;
yield next;
var ms = new Date - start;
console.log('%s %s - %s', this.method, this.url, ms);
});
// 響應中間件
app.use(function *(){
this.body = 'Hello World';
});
app.listen(3000);
上面的執行順序就是:請求 ==> response-time中間件 ==> logger中間件 ==> 響應中間件 ==> logger中間件 ==> response-time中間件 ==> 響應。
更詳細描述就是:請求進來,先進到response-time中間件,執行 var start = new Date; 然后遇到yield next,則暫停response-time中間件的執行,跳轉進logger中間件,同理,最后進入響應中間件,響應中間件中沒有yield next代碼,則開始逆序執行,也就是再先是回到logger中間件,執行yield next之后的代碼,執行完后再回到response-time中間件執行yield next之后的代碼。
至此,整個Koa的中間件執行完畢 ,整個中間件執行過程相當有意思。
而Koa的中間件是運行在 co 函數下的,而tj大神的co函數能夠把異步變同步,也就說,編寫Koa的中間件的時候可以這樣寫,就拿上面那個demo最后的響應中間件來說可以改成這樣:
app.use(function*(){
var text = yield new Promise(function(resolve){
fs.readFile('./index.html', 'utf-8', function(err, data){
resolve(data);
})
});
this.body = text;
});
通過Promise可以把獲取的文件數據data通過resolve函數,傳到最外層的text中,而且,整個異步操作變成了同步操作。
再比如使用mongodb做一個數據庫查詢功能,就可以寫成這樣,整個數據的查詢原來是異步操作,也可以變成了同步,因為mongodb官方驅動的接口提供了返回Promise的功能,在co函數里只用yield的時候能夠直接把異步變成同步,再也不用寫那惡心的回調嵌套了。
var MongoClient = require("mongodb").MongoClient;
app.use(function *(){
var db = yield MongoClient.connect('mongodb://127.0.0.1:27017/myblog');
var collection = db.collection('document');
var result = yield collection.find({}).toArray();
db.close()
});
tj的co函數就如同一個魔法,把所有異步都變成了同步,看起來好像很高大上。但是co函數做的事其實並不復雜。
整個co函數說白了,就是使用Promise遞歸調用generator的next方法,並且在后一次調用的時候把前一次返回的數據傳入,直到調用完畢。而co函數同時把非Promise對象的function、generator、array等也組裝成了Promise對象。所以可以在yield后面不僅僅可以接Promise,還可以接generator對象等。
自己實現了一個簡單的co函數,傳入一個generator,獲取generator的函數對象,然后定義一個next方法用於遞歸,在next方法里執行generator.next()並且傳入data,執行完generator.next()會獲取到{value:XX, done: true|false}的對象,如果done為true,說明generator已經迭代完畢,退出。
否則,假設當前執行到yield new Promise(),也就是返回的result.value就是Promise對象的,直接執行Promise的then方法,並且在then方法的onFulfilled回調(也就是Promise中的異步執行完畢后,調用resolve的時候會觸發該回調函數)中執行next方法進行遞歸,並且將onFulfilled中傳入的數據傳入next方法,也就可以在下一次generator.next()中把數據傳進去。
// co簡易實現
function co(generator){
var gen = generator();
var next = function(data){
var result = gen.next(data);
if(result.done) return;
if (result.value instanceof Promise) {
result.value.then(function (d) {
next(d);
}, function (err) {
next(err);
})
}else {
next();
}
};
next();
}
寫個demo測試一下:
// test
co(function*(){
var text1 = yield new Promise(function(resolve){
setTimeout(function(){
resolve("I am text1");
}, 1000);
});
console.log(text1);
var text2 = yield new Promise(function(resolve){
setTimeout(function(){
resolve("I am text2");
}, 1000);
});
console.log(text2);
});
運行結果:
運行成功!
既然了解了co函數的原理,再來說說koa的中間件是怎么實現的。整個實現原理就是把所有generator放到一個數組里保存,然后對所有generator進行相應的鏈式調用。
起初是自己按照自己的想法實現了一次,大概原理如下:
用個數組,在每次執行use方法的時候把generator傳入gens數組保存,然后在執行的時候,先定義一個generator的執行索引index、跳轉標記ne(也就是yield next里的next)、還有一個是用於保存generator函數對象的數組gs,。然后獲取當前中間件generator,並且獲取到該generator的函數對象,將函數對象放入gs數組中保存,再執行generator.next()。
接着根據返回的value,做不同處理,如果是Promise,則跟上面的co函數一樣,在其onFulfilled的回調中執行下一次generator.next(),如果是ne,也就是當前執行到了yield next,說明要跳轉到下一個中間件,此時對index++,然后從gens數組里獲取下一個中間件重復上一個中間件的操作。
當執行到的中間件里沒有yield next時,並且當該generator已經執行完畢,也就是返回的done為true的時候,再逆序執行,從此前用於保存generator的函數對象gs數組獲取到上一個generator函數對象,然后執行該generator的next方法。直到全部執行完畢。
整個過程就像,先是入棧,然后出棧的操作。
//簡易實現koa的中間件效果
var gens = [];
function use(generetor){
gens.push(generetor);
}
function trigger(){
var index = 0;
var ne = {};
var gs = [],
g;
next();
function next(){
//獲取當前中間件,傳入next標記,即當yield next時處理下一個中間件
var gen = gens[index](ne);
//保存實例化的中間件
gs.push(gen);
co(gen)
}
function co(gen, data){
if(!gen) return;
var result = gen.next(data);
// 當當前的generator中間件執行完畢,將執行索引減一,獲取上一級的中間件並且執行
if(result.done){
index--;
if(g = gs[index]){
co(g);
}
return;
}
// 如果執行到Promise,則當Promise執行完畢再進行遞歸
if(result.value instanceof Promise){
result.value.then(function(data){
co(gen, data);
})
}else if(result.value === ne){
// 當遇到yield next時,執行下一個中間件
index++;
next();
}else {
co(gen);
}
}
}
然后再寫個demo測試一下:
// test
use(function*(next){
var d = yield new Promise(function(resolve){
setTimeout(function(){
resolve("step1")
}, 1000)
});
console.log(d);
yield next;
console.log("step2");
});
use(function*(next){
console.log("step3");
yield next;
var d = yield new Promise(function(resolve){
setTimeout(function(){
resolve("step4")
}, 1000)
});
console.log(d);
});
use(function*(){
var d = yield new Promise(function(resolve){
setTimeout(function(){
resolve("step5")
}, 1000)
});
console.log(d);
console.log("step6");
});
trigger();
運行結果:
運行成功!
上面的只是我自己的覺得的實現原理,但是其實koa自己的實現更精簡,在看了koa的源碼后,也大概實現了一下,其實就是把上面的那個co函數進行適當改造一下,然后用個while循環,把所有generator鏈式綁定起來,再放到co函數里進行yield即可。下面貼出源碼:
var gens = [];
function use(generetor){
gens.push(generetor);
}
// 實現co函數
function co(flow, isGenerator){
var gen;
if (isGenerator) {
gen = flow;
} else {
gen = flow();
}
return new Promise(function(resolve){
var next = function(data){
var result = gen.next(data);
var value = result.value;
// 如果調用完畢,調用resolve
if(result.done){
resolve(value);
return;
}
// 如果為yield后面接的為generator,傳入co進行遞歸,並且將promise返回
if (typeof value.next === "function" && typeof value.throw === "function") {
value = co(value, true);
}
if(value.then){
// 當promise執行完畢,調用next處理下一個yield
value.then(function(data){
next(data);
})
}
};
next();
});
}
function trigger(){
var prev = null;
var m = gens.length;
co(function*(){
while(m--){
// 形成鏈式generator
prev = gens[m].call(null, prev);
}
// 執行最外層generator方法
yield prev;
})
}
執行結果也是無問題,運行demo和運行結果跟上一個一樣,就不貼出來了。
順便貼出自己用來學習koa的項目:https://github.com/whxaxes/myblog 有興趣的可以一閱
上面寫的三個代碼也放在了github:
https://github.com/whxaxes/myblog/blob/master/myco.js
https://github.com/whxaxes/myblog/blob/master/mykoa.js
https://github.com/whxaxes/myblog/blob/master/mykoa_2.js
以及能幫助理解的文章:http://www.infoq.com/cn/articles/generator-and-asynchronous-programming/

