最近嘗試用了一下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和運行結果跟上一個一樣,就不貼出來了。
上面寫的三個代碼放在了github:
https://github.com/whxaxes/node-test/blob/master/other/myco.js
https://github.com/whxaxes/node-test/blob/master/other/mykoa.js
https://github.com/whxaxes/node-test/blob/master/other/mykoa_2.js
以及能幫助理解的文章:http://www.infoq.com/cn/articles/generator-and-asynchronous-programming/