本筆記共四篇
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
首先看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中,則使用一個變量,指向了prototype
。api
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())
中傳入的參數的做用。
在這裏,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
通常,request
和response
方法涇渭分明。同時在使用過程當中,是明顯有感受到Koa
比Express
要便利的。而不只僅是解決回調地獄那麼簡單。
在第一節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()方法。
但當時就是想不起來,結果睡了一覺就忽然領悟了。= =
最近也在上一門課,名稱就叫《學習如何學習》,裏面也有提到睡眠能幫本身整理記憶,遇到問題也不須要死鑽牛角尖,說不定過一下子答案會本身浮現的。
目前來看,確實是說的很對。
同時在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的接收響應並處理的圖,是這樣的:
到這一步,這些東西就好解釋了。
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的啓動和響應流程便完整的走了一遍。
有些感慨,也有些唏噓。
有不少想說的,但也感受沒什麼可說的。
就這樣吧。
前端路漫漫,且行且歌。
最後附上本人博客地址和原文連接,但願能與各位多多交流。