【服務端知識點】koa框架深入淺出

koa

學習koa需要一些相關知識,有兩個關鍵詞

  • generator
  • promise

本文主要針對koa的原理進行討論,屬於深度篇,並不會對koa的使用過多介紹。

如果在閱讀過程中,發現有哪些地方我寫的不太清楚,不容易理解,希望能提出,我會參考建議並進行修改~~

koa總體流程圖

讓我們從一張圖開始

上圖中,詳細說明了koa從啓動server之前,到接受請求在到響應請求的過程中,經歷了哪些步驟。

那我們按照時間線說起~

啓動前

圖中有三個藍色的方塊,分別代表三個靜態類

什麼是靜態類?這個是我自己給起的名,哈哈

靜態類就是程序運行前就存在的方法集合,動態類就是通過代碼生成出的方法集合。額,都是我自己起的名,概念也是我自己琢磨的,就是簡單歸個類。

三個靜態類分別是RequestContextResponse

Request

Request中包含了一些操作 Node原生請求對象的非常有用的方法。例如獲取query數據,獲取請求url等,更多方法去查API

Response

Response中包含了一些用於設置狀態碼啦,主體數據啦,header啦,等一些用於操作響應請求的方法。更多方法去查API

Context

Context是koa中最重要的概念之一,Context字面意思是上下文,也有環境等意思,koa中的操作都是基於這個context進行的,例如

this.body = 'hello world';

從前面的圖中,啓動前的三個藍色方塊可以看到,左邊的Request和右邊的Response各有一個箭頭指向Context,表示Request和Response自身的方法會委託到Context中。

Context中有兩部分,一部分是自身屬性,主要是應用於框架內部使用,一部分是Request和Response委託的操作方法,主要爲提供給用戶更方便從Request獲取想要的參數和更方便的設置Response內容。

下面是Context源碼片段。

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

/**
 * Response delegation.
 */

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('set')
  .method('append')
  .access('status')
  .access('message')
  .access('body')
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');

/**
 * Request delegation.
 */

delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  .method('is')
  .access('querystring')
  .access('idempotent')
  .access('socket')
  .access('search')
  .access('method')
  .access('query')
  .access('path')
  .access('url')
  .getter('origin')
  .getter('href')
  .getter('subdomains')
  .getter('protocol')
  .getter('host')
  .getter('hostname')
  .getter('header')
  .getter('headers')
  .getter('secure')
  .getter('stale')
  .getter('fresh')
  .getter('ips')
  .getter('ip');

delegates是第三方npm包,功能就是把一個對象上的方法,屬性委託到另一個對象上

對了,你猜對了,上面那一排方法,都是Request和Response靜態類中的方法,有點看目錄的感覺~

method方法是委託方法,getter方法用來委託getter,access方法委託getter+setter

下面是源碼片段

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(){
    return this[target][name].apply(this[target], arguments);
  };

  return this;
};

從上面的代碼中可以看到,它其實是在proto上新建一個與Request和Response上的方法名一樣的函數,然後執行這個函數的時候,這個函數在去Request和Response上去找對應的方法並執行。

簡單來個栗子

var proto = {};

var Request = {
  test: function () {
    console.log('test');
  }
};

var name = 'test';
proto[name] = function () {
  return Request[name].apply(Request, arguments);
};

我們在來看看getter方法

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

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

  return this;
};

可以看到,在proto上綁定個getter函數,當函數被觸發的時候去,會去對應的request或response中去讀取對應的屬性,這樣request或response的getter同樣會被觸發~

我們在來看看access

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

可以看到,這個方法是getter+setter,getter上面剛說過,setter與getter同理,不多說了,心好累....

應用啓動前的內容到現在就說完了,接下來我們看看使用koa來啓動一個app的時候,koa內部會發生什麼呢?

啓動server

我們使用koa來啓動server的時候有兩個步驟。第一步是init一個app對象,第二步是用app對象監聽下端口號,一個server就啓動好了。

// 第一步 - 初始化app對象
var koa = require('koa');
var app = koa();

// 第二步 - 監聽端口
app.listen(1995);

簡單吧?

不瞭解內部機制的同學,通常會認爲server是在koa()這個時候啓動的,app.listen只是監聽下端口而已~

事實上。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。並不是。

有木有被刷新三觀???

我們看下源碼片段

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);
}

從源碼中可以看到,執行koa()的時候初始化了一些很有用的東西,包括初始化一個空的中間件集合,基於Request,Response,Context爲原型,生成實例等操作。

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這個方法上。

我們一起看一下this.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);
    fn.call(ctx).then(function () {
      respond.call(ctx);
    }).catch(ctx.onerror);
  }
};

這個方法其實可以分成兩部分,一部分是執行函數的那一瞬間所執行的代碼,另一部分是接收請求的時候所執行的代碼。

而前一部分就是總體流程圖中,啓動server這個時間段,黃色橢圓形所執行的那一部分,初始化中間件!!!

第一部分

先說第一部分,很明顯,這環節是在初始化中間件,那爲什麼要初始化中間件呢?處理後的中間件與處理之前的中間件又有什麼不同呢????

童鞋,,,不要着急,聽我慢慢道來~~

我們添加中間的時候使用app.use方法,其實這個方法只是把中間件push到一個數組,然後就沒有然後了。。(⊙﹏⊙)

很明顯,所有中間件都在數組中,那麼它們之間是沒有聯繫的,如果沒有聯繫,就不可能實現流水線這樣的功能。。。。

那麼這些中間件處理之後會變成什麼樣的????

我們先看代碼,上面的代碼中用this.experimental這個屬性做了一個判斷。這個屬性是什麼鳥。

this.experimental 關於這個屬性我並沒有在官方文檔上看到說明,但以我對koa的瞭解,這個方法是爲了判斷是否支持es7,默認是不支持的,如果想支持,需要在代碼中明確指定this.experimental = true,開啓這個屬性之後,中間件可以傳入async函數。

我想說的是,無論是否開啓ES7,原理都是相同的,只是因爲語法特性的不同,需要不同的處理,核心思想不會因爲不同的語言特性而改變,支持ES7顯然處理起來更方便,因爲默認不開啓this.experimental,所以這裏我們針對不開啓的情況進行討論~

這樣一來,第一部分的代碼就簡化成了這樣

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

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

我們先看compose(this.middleware)這部分,compose的全名叫koa-compose,他的作用是把一個個不相干的中間件串聯在一起。。

例如

// 有3箇中間件
this.middlewares = [function *m1() {}, function *m2() {}, function *m3() {}];

// 通過compose轉換
var middleware = compose(this.middlewares);

// 轉換後得到的middleware是這個樣子的
function *() {
  yield *m1(m2(m3(noop())))
}

有木有很神奇的感覺??更神奇的是,generator函數的特性是,第一次執行並不會執行函數裏的代碼,而是生成一個generator對象,這個對象有next,throw等方法。

這就造成了一個現象,每個中間件都會有一個參數,這個參數就是下一個中間件執行後,生成出來的generator對象,沒錯,這就是大名鼎鼎的 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--) {
      next = middleware[i].call(this, next);
    }

    return yield *next;
  }
}

/**
 * Noop.
 *
 * @api private
 */

function *noop(){}

這是這個模塊的所有代碼,很簡單,邏輯是這樣的

先把中間件從後往前依次執行,並把每一箇中間件執行後得到的generator對象賦值給變量next,當下一次執行中間件的時候(也就是執行前一箇中間件的時候),把next傳給第一個參數。這樣就保證前一箇中間件的參數是下一個中間件生成的generator對象,第一次執行的時候next爲noopnoop是空的generator函數。

koa的中間件必須爲generator函數(就是帶星號的函數),否則無法順利的執行中間件邏輯

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

這行代碼可以實現把compose執行後return的函數變成第一個中間件,也就是說,執行compose之後會得到一個函數,執行這個函數就與執行第一個中間件的效果是一模一樣的,這主要依賴了generator函數的yield *語句的特性。

現在中間件的狀態就已經從不可用變成可用了。不可用的中間件是一個數組,可用的中間件是一個generator函數

我們接着說剛纔沒說完的

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

上面這段代碼現在就可以理解成下面這樣

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

裏面的函數剛剛已經說過是可用狀態的中間件,那麼co.wrap是幹什麼用的呢??

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並傳入一個參數,這個參數是中間件函數執行後生成的Generator對象。

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

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);
  }
};

從源碼中,可以看到這個函數賦值給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
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的原型對象

不多說,咱們觀摩下代碼

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

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

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

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;
  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) {
      if (ret.done) return resolve(ret.value);
      var value = 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) + '"'));
    }
  });
}

可以看到,代碼並不是很多。

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

我們在看這段代碼

function onFulfilled(res) {
  var ret;
  try {
    ret = gen.next(res);
  } catch (e) {
    return reject(e);
  }
  next(ret);
}

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

通常在開發中間件的時候會這樣寫

yield next;

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

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

function next(ret) {
  if (ret.done) return resolve(ret.value);
  var value = 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) + '"'));
}

可以看到,邏輯是這樣的

如果中間件已經結束(沒有yield了),那麼調用promise的resolve。

否則的話把ret.value(就是下一個中間件對象),用co在包一層toPromise.call(ctx, 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方法。

所以有一個非常重要的一點需要注意,onFulfilled這個函數非常重要,重要在哪裏???重要在它執行的時間上。

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

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

回逆其實是有一個去和一個回的操作

請求的時候經過一次中間件,響應的時候在經過一次中間件。

而onFulfilled的兩種被調用的情況正好和這個回逆的過程對應上。

前方高能預警!!!

比如有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從裏往外跳。

所以如果我們在一箇中間件中寫好多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
});

這就是最核心的思想!!!

總結

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

其實就是通過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 - 上下文

最後說一些自己對koa的感覺,真他媽的是賞心悅目啊,真他媽的是優雅啊!!!每一行代碼都濃縮了很多層含義,通過最少的代碼實現最複雜的功能,對於我這種追求代碼的極致優雅的人,看完koa之後,真的是感觸良多,淚流滿面啊。。。。

ppt:http://berwin.github.io/ppts/koa/

轉載請註明出處

發佈了15 篇原創文章 · 獲贊 25 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章