koa 核心源碼介紹

 連接來源javascript

 

RequestContextResponse  在代碼運行以前就已經存在java

Request和Response自身的方法會委託到Context中。node

 

Context源碼片斷git

var delegate = require('delegates');
var proto = module.exports = {}; // 一些自身方法,被我刪了

/**
 * Response delegation.
 */

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')

 

能夠看到, context 把 response 的方法加到 context本身的原型中來, 方便用戶直接經過context調用 response request的方法github

 

 delegate實現過程, 先在構造函數中存儲context的原型 proto,  再method中更加name 把 response原型的方法追加到 context中json

function Delegator(proto, target) {
  if (!(this instanceof Delegator)) return new Delegator(proto, target);
  this.proto = proto;
  this.target = target;
  this.methods = [];
  this.getters = [];
  this.setters = [];
  this.fluents = [];
}

Delegator.prototype.method = function(name){
  var proto = this.proto;
  var target = this.target;
  this.methods.push(name);

  proto[name] = function(){
   //Response[name].apply(Response, arguments); 至關下面  
return this[target][name].apply(this[target], arguments); }; return this; };

 

上面是委託了 Response Request的方法, 其實context還委託了 Response request的屬性名稱, api

能夠用context直接訪問他們的屬性的值,    用context設置屬性的值的時候, Response request的屬性值也會改變promise

分別是用getter setter實現的服務器

 

Delegator.prototype.getter = function(name){
  var proto = this.proto;
  var target = this.target;
  this.getters.push(name);

  proto.__defineGetter__(name, function(){
   //Response[name]
return this[target][name]; }); return this; };

 

咱們在來看看accesscookie

Delegator.prototype.access = function(name){ return this.getter(name).setter(name); };

能夠看到,這個方法是getter+setter

應用啓動前的內容到如今就說完了

 

接下來咱們看看使用 koa來啓動一個app的時候,koa內部會發生什麼呢?

啓動server

會通過兩步

第一步是 new 一個 koa 對象app

第二步是用 app對象 監聽端口號

 

// 第一步 - 初始化app對象 var koa = require('koa'); var app = koa(); // 第二步 - 監聽端口 app.listen(1995);

  能夠看一下源碼 koa() 這裏發生了什麼

module.exports = Application; function Application() {

if (!(this instanceof Application)) return new Application; this.env = process.env.NODE_ENV || 'development'; this.subdomainOffset = 2;

this.middleware = []; this.proxy = false;

this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); }

能夠看到, 這裏只是定義了一個空的中間件對象, 並把context request等的實例掛載到application中

這裏應爲實例化了context, 因此會執行delegate方法,因此這一步, 也是 正式把request response委託到context上

到了這裏並沒啓動server, 直到監聽的時候

 

app.listen = function(){

debug('listen'); var server = http.createServer(this.callback()); return server.listen.apply(server, arguments);

};

能夠看到,在執行app.listen(1995)的時候,啓動了一個server,而且監聽端口。

熟悉nodejs的同窗知道http.createServer接收一個函數做爲參數,每次服務器接收到請求都會執行這個函數,

這個函數會傳入兩個參數(request和response,簡稱req和res),那麼如今重點在this.callback這個方法上。

 

好比 http.createServer( funciton(res, req){ req.send('text') }  //callback),

koa利用 listen 包裝了這個函數, 使用listen直接啓動服務, 並把callback包裝一下 ,注入本身的東西, 這裏其實就是初始化了一些中間件

爲何要初始化呢, 咱們是使用app.use(log)添加一個日誌中間件, 這裏只是push一箇中間件, 而初始化就是使這些中間件聯繫起來

 

app.callback 源碼

 

app.callback = function(){
  
  //咱們能夠指定 app.experimental = true, 這樣就開啓了es7的一些功能, 好比咱們可使用 async await 了 
if (this.experimental) { console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.') }
 
//這裏分析的時候簡化爲 co.wrap(compose(this.middleware)); 由於兩個分支的核心思想是同樣的, es7更簡化了而已
var fn = this.experimental ? compose_es7(this.middleware) : co.wrap(compose(this.middleware));
var self = this; if (!this.listeners('error').length) this.on('error', this.onerror); return function(req, res){
res.statusCode
= 404; var ctx = self.createContext(req, res); onFinished(res, ctx.onerror);
fn.call(ctx).then(
function () { respond.call(ctx); }).catch(ctx.onerror);
}
};

 

var fn = co.wrap(compose(this.middleware));


雖然只剩下一行代碼,但不要小瞧它哦~~


咱們先看這部分,的全名叫,他的做用是把一個個不相干的中間件串聯在一塊兒。


compose(this.middleware)composekoa-compose
// 有3箇中間件 this.middlewares = [function *m1() {}, function *m2() {}, function *m3() {}]; // 經過compose轉換 var middleware = compose(this.middlewares); // 轉換後獲得的middleware是這個樣子的  function *() { yield *m1(m2(m3(noop()))) }

   

相似: 

function a(s){ console.log(s) }
 
function b(s){console.log(s); return s+1}
 
function c(s){console.log(s); return s+2}

a( b(c(0)) );

 

上面這個函數執行的時候遇到 yield, 因此 *m1不會執行,  當調用next後, 執行順序 noop() => m3() => m2( ) => m1()

 

 yield *m1(m2(m3(noop())))

 但m3是一個generator函數, 因此它第一次執行, 實際上裏面的代碼是不會執行的, 直到你調用 next,  

 因此當你把 m3() 執行後返回的一個對象做爲m2的參數時, 這個參數暫時命名爲 p1, 使得有條件在m2中調用 p1.next 

 來真正執行m3裏面的代碼 , 沒錯,這就是大名鼎鼎的 中間件 next

 

咱們是這樣利用中間件的

var app = require('koa')(),
    router = require('koa-router')(); app.use(function *(next){ console.log(1); yield next; console.log(5); }); app.use(function *(next){ console.log(2);
//實際上這個next 就是m3第一次執行完之後的generator對象, 當console.log(2)執行完後, 再koa會調用app.middleware.next方法, 則把yield後面的 next執行了 yield next; console.log(4); }); app.use(function *(){ console.log(3); }); app.listen(3000);

當一個請求到達的時候,控制檯會依次輸出1 2 3 4 5這就是koa中強大的middleware特性

 

能夠中斷當前代碼的執行, 去執行下一個中間件B的代碼, 等下一個中間件B的代碼執行完成, 再去執行剩餘的,  

同時B也能夠中斷其執行下一個中間件C, 從而使得這些中間件能夠竄起來使用, 一箇中間件內部直接利用另外一箇中間件的結果,

 

 

問題:

若是另外一箇中間件是異步? 估計也會內部增長一個yield, 多調用一次next拿到異步數據,  + 哦,  後面會解釋道, 好像是利用promise 保證異步完成, 才調用返回上一個中間件的.

 

執行如下代碼, 能夠更瞭解 yield特性

function* numbers() {
    console.log('function start.');

    var v1 = yield 0;
    console.log('v1 = ' + v1);

    var v2 = yield 1;
    console.log('v2 = ' + v2);

    return 5;
}

var nums = numbers();
//nums.next(); nums.next(); nums.next();

 

compose 實現過程/** * Expose compositor. */

 module.exports = compose; /** * Compose `middleware` returning * a fully valid middleware comprised * of all those which are passed. * * @param {Array} middleware * @return {Function} * @api public */ function compose(middleware){
return function *(next){
if (!next) next = noop(); var i = middleware.length;
  //先把中間件從後往前依次執行

while (i--) {
//把每個中間件執行後獲得的 generator對象 賦值給變量next next
= middleware[i].call(this, next); } return yield *next; } } /** * Noop. * * @api private */ function *noop(){}

 

最後,有一個很是巧妙的地方,就是最後一行return yield *next;

 

這行代碼能夠實現把compose執行後 return的函數變成第一個中間件,(由於第一個中間件其實在while循環中執行了一次)

也就是說,執行compose以後會獲得一個函數,執行這個函數就與執行第一個中間件的效果是如出一轍的,

這主要依賴了generator函數的yield *語句的特性。

 

 

咱們接着說剛纔沒說完的

var fn = co.wrap(compose(this.middleware));

上面這段代碼如今就能夠理解成下面這樣

var fn = co.wrap(function *() {yield *m1(m2(m3(noop())))});

 co是TJ大神基於Generator開發的一款流程控制模塊,白話文就是:就是把異步變成同步的模塊。。。

 

 

看下源碼

co.wrap = function (fn) {
createPromise.__generatorFunction__ = fn; return createPromise;
function createPromise() { return co.call(this, fn.apply(this, arguments)); } };

從源碼中能夠看到,它接收一個參數,這個參數就是可用狀態下的中間件,

返回一個函數createPromise,當執行createPromise這個函數的時候,

調用co並傳入一個參數,這個參數 fn 是中間件函數執行後生成的 Generator對象 。

 

這意味着,返回的這個函數 createPromise 是 觸發執行中間件邏輯的關鍵,一旦這個函數被執行,那麼就會開始執行中間件邏輯

回到callback代碼

app.callback = function(){
if (this.experimental) { console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.') }
var fn = this.experimental ? compose_es7(this.middleware) : co.wrap(compose(this.middleware)); var self = this; if (!this.listeners('error').length) this.on('error', this.onerror);
//最後的等待函數
return function(req, res){
res.statusCode
= 404; var ctx = self.createContext(req, res); onFinished(res, ctx.onerror);

//執行這裏 實際上就是執行了createPromise函數, 也就是執行了第一個中間件 fn.call(ctx).then(
function () { respond.call(ctx); }).catch(ctx.onerror); } };

從源碼中,能夠看到這個函數賦值給fn,fn是在下面那個函數中執行的,下面那個函數是接下來要說的內容~

到如今,咱們的koa已經處於一種待機狀態,全部準備都以準備好(中間件和context),萬事俱備,只欠東風。。。。。。

東風就是request請求~~

 

接收請求

前面說了啓動前的一些準備工做和啓動時的初始化工做,如今最後一步就是接收請求的時候,

koa要作的事情了,這部分也是koa中難度最大的一部分。不過認真閱讀下去會有收穫的。。

上面咱們說this.callback這個方法有兩個部分,第一個部分是初始化中間件,而另外一部分就是接收請求時執行的函數啦。

簡單回顧下

// 建立server並監聽端口
app.listen = function(){
  debug('listen');
  var server = http.createServer(this.callback());
return server.listen.apply(server, arguments); }; // 這個方法返回的函數會被傳遞到http.createServer中,
// http.createServer這個方法的做用是 : 每當服務器接收到請求的時候,都會執行第一個參數,而且會傳遞request和response
// listen 服務器是怎麼處理的, 如何傳resp resq 得深刻到底層了
app.callback = function(){ if (this.experimental) { console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.') } var fn = this.experimental ? compose_es7(this.middleware) : co.wrap(compose(this.middleware)); var self = this; if (!this.listeners('error').length) this.on('error', this.onerror); return function(req, res){
res.statusCode
= 404; var ctx = self.createContext(req, res); onFinished(res, ctx.onerror);
fn.call(ctx).then(
function () { respond.call(ctx); }).catch(ctx.onerror);
} };

 

因此第二部分的重點就是下面段代碼啦~

return function(req, res){ 
res.statusCode = 404; var ctx = self.createContext(req, res); onFinished(res, ctx.onerror);
fn.call(ctx).then(function () { respond.call(ctx); }).catch(ctx.onerror);
}

咱們先看這段代碼

var ctx = self.createContext(req, res);

 

不知道各位童鞋還記不記得文章一開始的時候那個整體流程圖下面的那個相似於八卦同樣的東西???

這行代碼就是建立一個最終可用版的context。

 

   從上圖中,能夠看到分別有五個箭頭指向ctx,表示ctx上包含5個屬性,分別是request,response,req,res,app。

request和response也分別有5個箭頭 指向它們,因此也是 一樣的邏輯

這裏須要說明下

  • request - request繼承於Request靜態類,包含操做request的一些經常使用方法  (處於上層)
  • response - response繼承於Response靜態類,包含操做response的一些經常使用方法
  • req - nodejs原生的request對象  (處於底層)
  • res - nodejs原生的response對象
  • app - koa的原型對象
 
var ctx = self.createContext(req, res);
 

很少說,我們觀摩下代碼

 

app.createContext = function(req, res){

  // 繼承
  var context = Object.create(this.context);
  var request = context.request = Object.create(this.request);
  var response = context.response = Object.create(this.response);

  // 往context,request,response身上掛載屬性 實現上圖 的關係 效果
  context.app = request.app = response.app = this;
  context.req = request.req = response.req = req;
  context.res = request.res = response.res = res;
request.ctx
= response.ctx = context; request.response = response; response.request = request;
context.onerror
= context.onerror.bind(context); context.originalUrl = request.originalUrl = req.url;
context.cookies
= new Cookies(req, res, { keys: this.keys, secure: request.secure });
context.accept
= request.accept = accepts(req); context.state = {}; // 最後返回完整版context return context;
};

講到這裏其實我能夠很明確的告訴你們,,,koa中的this其實就是app.createContext方法返回的完整版context

又因爲這段代碼的執行時間是接受請求的時候,因此代表每一次接受到請求,都會爲該請求生成一個新的上下文context

上下文到這裏咱們就說完啦。咱們接着往下說,看下一行代碼

 

onFinished(res, ctx.onerror);

這行代碼其實很簡單,就是監聽response,若是response有錯誤

會執行ctx.onerror中的邏輯,設置response類型,狀態碼和錯誤信息等

源碼以下:

onerror: function(err){ // don't do anything if there is no error. // this allows you to pass `this.onerror` // to node-style callbacks. if (null == err) return; if (!(err instanceof Error)) err = new Error('non-error thrown: ' + err); // delegate this.app.emit('error', err, this); // nothing we can do here other // than delegate to the app-level // handler and log. if (this.headerSent || !this.writable) { err.headerSent = true; return; } // unset all headers this.res._headers = {}; // force text/plain this.type = 'text'; // ENOENT support if ('ENOENT' == err.code) err.status = 404; // default to 500 if ('number' != typeof err.status || !statuses[err.status]) err.status = 500; // respond var code = statuses[err.status]; var msg = err.expose ? err.message : code; this.status = err.status; this.length = Buffer.byteLength(msg); this.res.end(msg); }

咱們接着說,還有最後一個知識點,也是本章最複雜的知識點,關於中間件的執行流程,這裏會說明爲何koa的中間件能夠回逆

咱們先看代碼

 

咱們先看代碼

fn.call(ctx).then(function () { respond.call(ctx); }).catch(ctx.onerror);
  • fn - 咱們上面講的 co.wrap 返回的那個函數
  • ctx - app.createContext執行後返回的完整版context對象

整體上來講,執行fn.call(ctx)會返回promise,koa會監聽執行的成功和失敗,成功則執行respond.call(ctx);,失敗則執行ctx.onerror

失敗的回調函數剛剛已經講過。這裏先說說respond.call(ctx);

 

咱們在寫koa的時候,會發現全部的response操做都是

 

this.body = xxx; this.status = xxxx;這樣的語法,但若是對原生nodejs有了解的童鞋知道,

nodejs的response只有一個api那就是res.end();,而設置status狀態碼什麼的都有不一樣的api,

那麼koa是如何作到經過this.xxx = xxx來設置response的呢?

 

先看一張圖,,我盜的圖

從圖中看到,request請求是以respond結束的。

是滴,全部的request請求都是以respond這個函數結束的,

這個函數會 讀取this.body中的值 根據不一樣的類型來決定以什麼類型響應請求

 

咱們來欣賞一下源碼

function respond() { // allow bypassing koa if (false === this.respond) return; var res = this.res; if (res.headersSent || !this.writable) return; var body = this.body; var code = this.status; // ignore body if (statuses.empty[code]) { // strip headers this.body = null; return res.end(); } if ('HEAD' == this.method) { if (isJSON(body)) this.length = Buffer.byteLength(JSON.stringify(body)); return res.end(); } // status body if (null == body) { this.type = 'text'; body = this.message || String(code); this.length = Buffer.byteLength(body); return res.end(body); } // responses if (Buffer.isBuffer(body)) return res.end(body); if ('string' == typeof body) return res.end(body); if (body instanceof Stream) return body.pipe(res); // body: json body = JSON.stringify(body); this.length = Buffer.byteLength(body); res.end(body); }

仔細閱讀的童鞋會發現,咦,,,,爲毛沒有設置status和header等信息的代碼邏輯

這不科學啊。我分明記得狀態碼是rs.statusCode = 400這樣設置的,爲啥代碼中沒有??

 

這就要從最開始的上下文提及了。爲何Response靜態類中添加req和res屬性?

就是由於添加了req和res以後,response和request類就能夠直接操做req和res啦。。咱們看一段源碼就明白了

set status(code) {
assert('number' == typeof code, 'status code must be a number'); assert(statuses[code], 'invalid status code: ' + code); this._explicitStatus = true;
this.res.statusCode = code; this.res.statusMessage = statuses[code]; if (this.body && statuses.empty[code]) this.body = null;
},

主要是this.res.statusCode = code; this.res.statusMessage = statuses[code];這兩句,

statusCodestatusMessage都是nodejs原生api。有興趣能夠自行查看~

接下來咱們開始說說koa的中間件爲何能夠回逆,爲何koa的中間件必須使用generator,yield next又是個什麼鬼?

 

咱們看這段代碼

fn.call(ctx)

fn剛剛上面說過,就是co.wrap返回的那個函數,上面也說過,一旦這個函數執行,就會執行中間件邏輯,而且經過.callctx設爲上下文,也就是this。

 

那中間件邏輯是什麼樣的呢。咱們先看一下源碼:

co.wrap = function (fn) { createPromise.__generatorFunction__ = fn; return createPromise;
function createPromise() { return co.call(this, fn.apply(this, arguments)); }
};

先回顧下,createPromise就是fn,每當執行createPromise的時候,都會執行co,

 

中間件是基於co實現的、因此咱們接下來要說的是co的實現邏輯。

而執行co所傳遞的那個參數,咱們給它起個名,就叫中間件函數吧,中間件函數也是一個generator函數,

由於在執行co的時候執行了這個中間件函數,因此實際上真正傳遞給co的參數是一個generator對象,爲了方便理解,咱們先起個名叫中間件對象吧

 

那咱們看co的源碼:

function co(gen) {
var ctx = this;
//獲取 gen 後面的參數 var args = slice.call(arguments, 1) // we wrap everything in a promise to avoid promise chaining, // which leads to memory leak errors. // see https://github.com/tj/co/issues/180
return new Promise(function(resolve, reject) { if (typeof gen === 'function') gen = gen.apply(ctx, args); if (!gen || typeof gen.next !== 'function') return resolve(gen); onFulfilled(); /** * @param {Mixed} res * @return {Promise} * @api private */ function onFulfilled(res) { var ret; try { ret = gen.next(res); } catch (e) { return reject(e); } next(ret); } /** * @param {Error} err * @return {Promise} * @api private */ function onRejected(err) { var ret; try { ret = gen.throw(err); } catch (e) { return reject(e); } next(ret); } /** * Get the next value in the generator, * return a promise. * * @param {Object} ret * @return {Promise} * @api private */ function next(ret) {
//若是一箇中間件已經完成全部的 yield, 回到上一個中間件繼續執行 if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
//若是還有yield 則繼續調用 then方法, 其實會調用next方法, 執行剩餘的 yield
    //
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); <==> tomPromise.call
 if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' + 'but the following object was passed: "' + String(ret.value) + '"')); } }); }

能夠看到,代碼並非不少。

首先執行co會返回一個promise,koa會對這個promise的成功和失敗都準備了不一樣的處理,上面已經說過。

 

咱們在看這段代碼

function onFulfilled(res) { var ret;
try {
//按順序執行, 從第一個中間件開始, 遇到 yield next 才執行下一個 ret = gen.next(res); } catch (e) { return reject(e); }
next(ret);
}

這個函數最重要的做用是運行gen.next執行 中間件中的 業務邏輯

一般在開發中間件的時候會這樣寫

yield next;

因此ret中包含下一個中間件對象(還記得上面咱們初始化中間件的時候中間件的參數是什麼了嗎??)

而後把下一個中間件對象傳到了next(ret)這個函數裏,next函數是幹什麼的?咱們看看

 

function next(ret) {

//若是中間件已經結束(沒有yield了),那麼調用promise的resolve。 if (ret.done) return resolve(ret.value);
  
//不然的話把ret.value(就是下一個中間件對象),用co在包一層 var value = toPromise.call(ctx, ret.value); //
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);toPromise.call(ctx, ret.value);

if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected( new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"')); }

 

if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);

上面是toPromise中的一段代碼

 

既然是用co又執行了一遍,那麼co是返回promise的。因此返回的  這個value就分別被監聽了 成功和失敗 的不一樣處理。

value.then(onFulfilled, onRejected);

因此咱們能夠看到,若是第二個中間件裏依然有yield next這樣的語句,那麼第三個中間件依然會被co包裹一層並運行.next方法,依次列推,這是一個遞歸的操做

因此咱們能夠確定的是,每個中間件都被promise包裹着,直到有一天中間件中的邏輯運行完成了,那麼會調用promise的resolve來告訴程序這個中間件執行完了。

那麼中間件執行完了以後,會觸發onFulfilled,這個函數會執行.next方法。

 

前方高能預警!!!

 

好比有3箇中間件,當系統接收到請求的時候,會執行co,co會馬上執行onFulfilled來調用.next往下執行,

將獲得的返回結果(第二個中間件的generator對象,上面咱們分析過)傳到co中在執行一遍。以此類推,一直運行到最後一個yield,

這個時候系統會等待中間件的執行結束,一旦最後一箇中間件執行完畢,會馬上調用promise的resolve方法表示結束。

 

(這個時候onFulfilled函數的第二個執行時機到了,這樣就會出現一個現象,

一個generator對象的yield只能被next一次,下次執行.next的時候從上一次停頓的yield處繼續執行,

因此如今當有一箇中間件執行完畢後,在執行.next就會在前一箇中間件的yield處繼續執行)

 

當最後一箇中間件執行完畢後,觸發promise的resolve,而別忘了,第二個中間件但是用then監聽了成功和失敗的不一樣處理方法,

一旦第三個中間件觸發成功,第二個中間件會馬上調用onFulfilled來執行.next,

繼續從第二個中間件上一次yield停頓處開始執行下面的代碼,而第二個中間件的邏輯執行完畢後,

 

一樣會執行resolve表示成功,而這個時候第一個中間件正好也經過.then方法監聽了第二個中間件的promise,

也會馬上調用onFulfilled函數來執行.next方法,這樣就會繼續從第一個中間件上一次yield的停頓處繼續執行下面的邏輯,以此類推。

這樣就實現了中間件的回逆,經過遞歸從外到裏執行一遍中間件,而後在經過promise+generator從裏往外跳。

 

因此有一個很是重要的一點須要注意,onFulfilled這個函數很是重要,重要在哪裏???重要在它執行的時間上。

onFulfilled這個函數只在兩種狀況下被調用,一種是調用co的時候執行,還有一種是當前promise中的全部邏輯都執行完畢後執行

其實就這一句話就能說明koa的中間件爲何會回逆。

 

因此若是咱們在一箇中間件中寫好多yield,就能夠看出關鍵所在,

先經過遞歸從外往裏(從第一個中間件運行到最後一箇中間件)每次遇到yield next就會進入到下一個中間件執行,

當運行到最後發現沒有yield的時候,會跳回上一個中間件繼續執行yield後面的,結果發現又有一個yield next,

 

它會再次進入到下一個中間件,進入到下一個中間件後發現什麼都沒有,

由於yield的特性(一個generator對象的yield只能被next一次,下次執行.next的時候從上一次停頓的yield處繼續執行),

因此便又一次跳入上一個中間件來執行。以此類推。

 

咱們試一下:

var koa = require('koa'); var app = koa(); app.use(function* f1(next) { console.log('f1: pre next'); yield next; console.log('f1: post next'); yield next; console.log('f1: fuck'); }); app.use(function* f2(next) { console.log(' f2: pre next'); yield next; console.log(' f2: post next'); yield next; console.log(' f2: fuck'); }); app.use(function* f3(next) { console.log(' f3: pre next'); yield next; console.log(' f3: post next'); yield next; console.log(' f3: fuck'); }); app.use(function* (next) { console.log('hello world') this.body = 'hello world'; }); app.listen(3000);

上面的代碼打印的log是下面這樣的

f1: pre next
  f2: pre next f3: pre next hello world f3: post next f3: fuck f2: post next f2: fuck f1: post next f1: fuck

 

 

那麼我用白話文來講一下中間件的邏輯,大概是這樣的, 就是前面說的

 

第一個中間件代碼執行一半停在這了,觸發了第二個中間件的執行,第二個中間件執行了一半停在這了,觸發了第三個中間件的執行,

而後,,,,,,第一個中間件等第二個中間件,第二個中間件等第三個中間件,,,,,,第三個中間件所有執行完畢,

第二個中間件繼續執行後續代碼,第二個中間件代碼所有執行完畢,執行第一個中間件後續代碼,而後結束

 

用一張圖表示大概是這樣的。

middleware

爲了方便理解,僞代碼大概是下面這樣

new Promise(function(resolve, reject) { // 我是中間件1 yield new Promise(function(resolve, reject) { // 我是中間件2 yield new Promise(function(resolve, reject) { // 我是中間件3 yield new Promise(function(resolve, reject) { // 我是body }); // 我是中間件3 }); // 我是中間件2 }); // 我是中間件1 });

這就是最核心的思想!! 

 

最後這句話解釋的思想好像文章的前半部分均可以看出來了, 我以爲 難點在於若是後面的中間件也是異步的,調用了多個 yield ... 實現異步操做,

如何處理 .. 也是是利用了promise把generator進一步封裝... 等下一個中間件把yield執行完, 在調用resolve 去回到上一個中間件去執行剩餘的代碼

 

總結

簡單總結一下,其實也很簡單,只是第一次接觸的同窗可能暫時沒有理解透徹。

其實就是經過generator來暫停函數的執行邏輯來實現等待中間件的效果,經過監聽promise來觸發繼續執行函數邏輯,所謂的回逆也不過就是同步執行了下一個中間件罷了。

好比有幾個中間件,mw1,mw2,mw3,mwn...

站在mw1的角度上看,它是不須要關係mw2裏面有沒有mw3,它只須要關心mw2什麼時候執行完畢便可,當mw2執行完畢mw1繼續執行yield以後的代碼邏輯。其實很簡單,callback也是這個原理,當mw2執行完畢執行下callback,mw1是不須要關心mw2裏面到底是怎樣運行的,只要知道mw2執行完會執行回調就好了。mw2也是一樣的道理不須要關心mw3。

到這裏,關於koa咱們就已經差很少都說完了。固然還有一些細節沒有說,好比koa中的錯誤處理,但其實都是小問題啦,關於generator的錯誤處理部分弄明白了,天然就明白koa的錯誤處理是怎樣的。這裏就不在針對這些講述了,一次寫這麼多確實有點累,或許後期會補充進來吧。。

兩個重要技術點

最後,若是認真閱讀下來的同窗能感受出來,koa中有兩個最重要的地方,不管是使用上,仍是思想上,這兩個點都很是重要,koa也只有這兩個概念

  • Middleware - 中間件
  • Context - 上下文

連接來源

相關文章
相關標籤/搜索