最近嘗試用了一下Koa,並在此記錄一下使用心得。html
注意:本文是以讀者已經瞭解Generator和Promise爲前提在寫的,由於單單Generator和Promise都可以寫一篇博文來說解介紹了,因此就不在這裏贅述。網上資料不少,能夠自行查閱。node
Koa是Express原班人馬打造的一個更小,基於nodejs平臺的下一代web開發框架。Koa的精妙之處就在於其使用generator和promise,實現了一種更爲有趣的中間件系統,Koa的中間件是一系列generator函數的對象,執行起來有點相似於棧的結構,依次執行。同時也相似於Python的django框架的中間件系統,之前蘇千大神作分享的時候把這種模型稱做爲洋蔥模型。如圖:git
當一個請求過來的時候,會依次通過各個中間件進行處理,中間件跳轉的信號是yield next,當到某個中間件後,該中間件處理完不執行yield next的時候,而後就會逆序執行前面那些中間件剩下的邏輯。直接上個官網的例子:github
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中間件 ==> 響應。web
更詳細描述就是:請求進來,先進到response-time中間件,執行 var start = new Date; 而後遇到yield next,則暫停response-time中間件的執行,跳轉進logger中間件,同理,最後進入響應中間件,響應中間件中沒有yield next代碼,則開始逆序執行,也就是再先是回到logger中間件,執行yield next以後的代碼,執行完後再回到response-time中間件執行yield next以後的代碼。mongodb
至此,整個Koa的中間件執行完畢 ,整個中間件執行過程至關有意思。數據庫
而Koa的中間件是運行在 co 函數下的,而tj大神的co函數可以把異步變同步,也就說,編寫Koa的中間件的時候能夠這樣寫,就拿上面那個demo最後的響應中間件來講能夠改爲這樣:django
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的時候可以直接把異步變成同步,不再用寫那噁心的回調嵌套了。promise
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/