Koa框架實踐與中間件原理剖析

   最近嘗試用了一下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/

相關文章
相關標籤/搜索