Koa源碼閱讀筆記(3) -- 服務器の啓動與請求處理

本筆記共四篇
Koa源碼閱讀筆記(1) -- co
Koa源碼閱讀筆記(2) -- compose
Koa源碼閱讀筆記(3) -- 服務器の啓動與請求處理
Koa源碼閱讀筆記(4) -- ctx對象javascript

原由

前兩天閱讀了Koa的基礎co,和Koa中間件的基礎compose
而後這兩天走在路上也在思考一些Koa運行機制的問題,感受總算有點理通了。
今天就來解讀一下Koa啓動時,發生的一系列事情。前端

啓動

若是隻是單純用Koa,那麼啓動服務器是很方便的。
下面就是一個最簡單的Hello World的例子。java

var koa = require('koa')
var app = new koa()

app.use(function * (next) {
  this.set('Powered by', 'Koa2-Easy')
  yield next
})

app.use(function * (next) {
  this.body = 'Hello World!'
})

app.listen(3000)

在上一節對koa-compose的分析中,解決了我一個問題,那就是使用中間件時,那個next參數是如何來的。
這一節也會解決一個問題,那就是中間件中的this是如何來的。node

有意思的地方

無new也可以使用的構造函數

首先看Koa構造函數的源代碼:git

/**
 * Expose `Application`.
 */

module.exports = Application;

/**
 * Initialize a new `Application`.
 *
 * @api public
 */

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

Application函數內部的第一句頗有意思。es6

if (!(this instanceof Application)) return new Application;

由於是構造函數,但不少人會忘記使用new來初始化。可是在Koa,則作了一點小措施,從而達到了是否調用new都能初始化的效果。github

原型的寫法

關於原型的寫法,不少人確定不陌生。以Koa的Application爲例,平時若是要寫原型的屬性,那麼會是這樣寫的。segmentfault

function Application() {}
Application.prototype.listen = function () {}
Application.prototype.callback = function () {}

這樣寫的話,每次都須要寫冗長的Application.prototype
而在Koa中,則使用一個變量,指向了prototypeapi

var app = Application.prototype;
app.listen = function () {}
app.callback = function () {}

寫起來簡潔,看起來也簡潔。服務器

服務器の啓動流程

在Koa中,或者說一切Node.js的Web框架中,其底層都是Node.js HTTP模塊來構建的服務器。
那麼我就對這點產生了好奇,究竟是什麼,能讓發送給服務器的相應,被Koa等框架截獲,並進行相應處理。
同時在Koa框架中,調用listen方法才能啓動服務。
那麼服務器的啓動流程就從listen方法開始。

啓動服務器

首先是listen方法的源代碼

/**
 * Shorthand for:
 *
 *    http.createServer(app.callback()).listen(...)
 *
 * @param {Mixed} ...
 * @return {Server}
 * @api public
 */

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

不難看出,只有使用了listen方法,http服務纔會被真正的建立並啓動。
而查閱文檔,則看到在http.createServer(this.callback())中傳入的參數的做用。
2016-07-29_10:07:26.jpg
在這裏,server 每次接收到請求,就會將其傳入回調函數處理。
同時listen方法執行完畢時,server便開始監聽指定端口。
因此在這裏,callback便成爲一個新的重點。

處理響應

繼續放上callback的源代碼(刪除部分無用部分):

/**
 * Return a request handler callback
 * for node's native http server.
 *
 * @return {Function}
 * @api public
 */

app.callback = function(){
  var fn = 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);
  }
};

在這兒,Koa的註釋對這個函數的做用解釋的很清楚。

Return a request handler callback for node's native http server.

而這兒,對於閉包的應用則讓我眼前一亮。
因爲服務器啓動後,中間件是固定的,因此像初始化中間件,保持this引用,註冊事件這種無需屢次觸發或者高耗能事件,便放入閉包中好了。
一次建立,屢次使用。

說到這兒想起一個問題,上次NodeParty, Koa演講結束後,有人詢問Koa可否根據請求作到動態加載中間件,當時他沒回答出來。
就源代碼來看,是不能作到動態加載的。最多也只是在中間件內部作一些判斷,從而決定是否跳過。

往下繼續讀,則能夠看到這一行:

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

在context中,是把一些經常使用方法掛載至ctx這個對象中。
好比在koa中,直接調用this.body = 'Hello World'這種response的方法,或者經過this.path得到request的路徑都是可行的。
而不用像Express通常,requestresponse方法涇渭分明。同時在使用過程當中,是明顯有感受到KoaExpress要便利的。而不只僅是解決回調地獄那麼簡單。

中間件的處理

在第一節Koa源碼閱讀筆記(1) -- co中,已經解釋了co.wrap的做用。
這兒能夠再看一次compose函數的源代碼。

function compose(middleware){
  return function *(next){
    // next不存在時,調用一個空的generator函數
    if (!next) next = noop();

    var i = middleware.length;
    // 倒序處理中間件,給每一箇中間件傳入next參數
    // 而next則是下一個中間件
    while (i--) {
      next = middleware[i].call(this, next);
    }

    return yield *next;
  }
}

function *noop(){}

在這裏,中間件被倒序處理,保證第一個中間件的next參數爲第二個中間件函數,第二個的next參數則爲第三個中間件函數。以此類推。
而最後一個則以一個空的generator函數結尾。

在這兒,有想了好久纔想通的點,那就是next = middleware[i].call(this, next);時,middleware沒有返回值,爲何next參數等於下一個函數。
到後來纔想通,中間件都是generator函數。generaotr會返回一個指向內部狀態的指針對象。
這一點我在co的閱讀筆記用說起, 也在阮一峯的《ECMAScript 6入門》看到了。

不一樣的是,調用Generator函數後,該函數並不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象。須要手動調用它的next()方法。

但當時就是想不起來,結果睡了一覺就忽然領悟了。= =
最近也在上一門課,名稱就叫《學習如何學習》,裏面也有提到睡眠能幫本身整理記憶,遇到問題也不須要死鑽牛角尖,說不定過一下子答案會本身浮現的。
2016-07-29_10:36:39.jpg
目前來看,確實是說的很對。

同時在compose函數最後的部分,返回了一個yield *next;

經過翻閱 《ECMAScript 6入門》-- 可知。

若是在Generater函數內部,調用另外一個Generator函數,默認狀況下是沒有效果的。這個就須要用到yield*語句,用來在一個Generator函數裏面執行另外一個Generator函數。

也就是說,其實每次執行時,是這樣的:

co(function* (next) {
  if (!next) next = noop();

  var i = middleware.length;

  while (i--) {
    next = middleware[i].call(this, next);
  }

  return yield *next;
})

return yield *next, next做爲第一個中間件,會被執行。
若是碰到中間件中的next,則會被co繼續調用和執行。
由於在co中,碰到generator函數是這樣的:

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

固然,若是在某個中間件中,碰到了以yield形式調用的函數,則會按co的規則,一路調用下去。
當中間件調用時,會返回一個Promise,而Promise在co中,會經過onFulfilled函數,實現自動調用。
從而就造成了獨特的Koa風格。

有點迷糊的話,舉個具體的栗子:

var koa = require('koa')
var app = new koa()

app.use(function * (next) {
  console.log('middleware 1 start')
  yield next
  console.log('middleware 1 finished')
})

app.use(function * (next) {
  console.log('middleware 2 finished')
})

app.listen(3000)

當接收到響應時,首先輸出middleware 1 start,而後碰到了 yield next, next是下一個中間件,會被co處理爲Promise函數。
而當第二個中間件執行完畢時,Promise自動調用then函數,而then卻又是第一個中間件的onFulfilled函數。
那麼第一個中間件就會繼續向下執行。直到執行完成。

因此最後Koa的接收響應並處理的圖,是這樣的:
2016-07-29_11:28:35.jpg

中間件中的this

到這一步,這些東西就好解釋了。

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

fn是處理過的中間件函數,使用call將建立好的ctx對象做爲this傳入,就能夠實如今中間件中使用this來處理請求/響應。

其餘

在整個處理過程當中,心細的小夥伴還注意到了onFinished函數和respond函數。
onFinished函數是一個Node的模塊。地址
做用則是在請求結束或錯誤是自動調用。因此這兒把ctx.onerror這個錯誤處理函數傳入,防止請求就直接是錯的。

而respond則是koa內部的函數,用於處理在中間件內部通過處理的ctx對象,併發送響應。
至此,Koa的啓動和響應流程便完整的走了一遍。

結語

有些感慨,也有些唏噓。
有不少想說的,但也感受沒什麼可說的。
就這樣吧。


前端路漫漫,且行且歌。

最後附上本人博客地址和原文連接,但願能與各位多多交流。

Lxxyx的前端樂園
原文連接:Koa源碼閱讀筆記(3) -- 服務器の啓動與請求處理

相關文章
相關標籤/搜索