原本打算寫算法課設的,這學期課程過重,實在沒辦法更新技術博客,但仍是心裏慚愧,偶然看到 deno 羣有人談到Koa、nest.js、express 比對 ,就沒忍住去看了看 Koa 源碼,寫了篇水文。做者水平有限(實際上我還沒用過Koa 嘞,只是跑去看了官網的 Api),歡迎多多指錯,而後我去寫課設了~css
首先要說明的是我參見的源碼版本是 Koa 的第一個發佈版 (0.0.2)。文件結構很簡單,只有三個文件:application.js
、context.js
、status.js
,下面「依次」來談。html
咱們仍是先來看 Context 比較好。node
Koa Context 將 node 的
request
和response
對象封裝到單個對象中,爲編寫 Web 應用程序和 API 提供了許多有用的方法。 這些操做在 HTTP 服務器開發中頻繁使用,它們被添加到此級別而不是更高級別的框架,這將強制中間件從新實現此通用功能。git
Module dependenciesgithub
var debug = require('debug')('koa:context');
var Negotiator = require('negotiator');
var statuses = require('./status');
var qs = require('querystring');
var Stream = require('stream');
var fresh = require('fresh');
var http = require('http');
var path = require('path');
var mime = require('mime');
var basename = path.basename;
var extname = path.extname;
var url = require('url');
var parse = url.parse;
var stringify = url.format;
複製代碼
Context 這個部分的代碼量是最多的,可是能說的東西其實不多。在這部分,Koa 使用 訪問器屬性 getter/setter 封裝了許多 http 模塊中經常使用的方法到單個對象中,並傳入後面會提到的 app.context() 。web
Application 是Koa 的入口文件。算法
Module dependenciesshell
var debug = require('debug')('koa:application');
var Emitter = require('events').EventEmitter;
var compose = require('koa-compose');
var context = require('./context');
var Cookies = require('cookies');
var Stream = require('stream');
var http = require('http');
var co = require('co');
複製代碼
有意思的構造函數express
構造函數的第一行頗有意思:編程
if (!(this instanceof Application)) return new Application;
這一行的目的是防止用戶忘記使用 new 關鍵字, 在 class 關鍵字還沒有引入js的時代,使用構造函數就會存在這種語義隱患,由於它一般均可以被「正常」的看成普通函數來調用。
var app = Application.prototype;
exports = module.exports = Application;
function Application() {
if (!(this instanceof Application)) return new Application;
this.env = process.env.NODE_ENV || 'development';
this.on('error', this.onerror);
this.outputErrors = 'test' != this.env;
this.subdomainOffset = 2;
this.poweredBy = true;
this.jsonSpaces = 2;
this.middleware = [];
this.Context = createContext();
this.context(context);
}
複製代碼
Application 構造函數原型中包含如下幾個方法:
listen()
app.listen = function(){
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
複製代碼
能夠看到這裏不過是使用了 node的 http 模塊建立http服務的簡單語法糖 ,並沒有特殊之處。
use()
app.use = function(fn){
debug('use %s', fn.name || '-');
this.middleware.push(fn);
return this;
};
複製代碼
Koa 應用程序是一個包含一組中間件函數的對象,它是按照相似堆棧的方式組織和執行的。
經過這個函數給應用程序添加中間件方法。咱們注意到 app.use() 返回的是 this,所以能夠鏈式表達,這點在官方文檔中也有說明。
即:
app.use(someMiddleware)
.use(someOtherMiddleware)
.listen(3000)
複製代碼
context()
app.context = function(obj){
var ctx = this.Context.prototype;
var names = Object.getOwnPropertyNames(obj);
debug('context: %j', names);
names.forEach(function(name){
if (Object.getOwnPropertyDescriptor(ctx, name)) {
debug('context: overwriting %j', name);
}
var descriptor = Object.getOwnPropertyDescriptor(obj, name);
Object.defineProperty(ctx, name, descriptor);
});
return this;
};
複製代碼
這裏的 context 實際上也給用戶提供了給 Context 添加其餘屬性(DIY)的方法。
callback()
app.callback = function(){
var mw = [respond].concat(this.middleware);
var gen = compose(mw);
var self = this;
return function(req, res){
var ctx = new self.Context(self, req, res);
co.call(ctx, gen)(function(err){
if (err) ctx.onerror(err);
});
}
};
複製代碼
首先咱們來看看第二行代碼:
var mw = [respond].concat(this.middleware);
這是個啥?
在 applation.js 文件中還有個生成器函數 respond ,是一個 Response middleware。所以,這行代碼就是把 Response middleware 做爲 middleware 中的第一個元素罷了。
而後咱們要重點談的是 compose ,它引用自 koa-compose,我覺得這裏是Koa的精髓所在,代碼也很簡潔。
function c (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
return function (context, next) {
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
複製代碼
有些人說這裏的 koa-compose 發源於函數式編程,這點我認同,多少是有點FP中compose的影子,但實際上仍是有很大區別的,函數式編程中的compose 傳入的函數由右至左依次執行,強調數據經過函數組合在管道中流動,讓咱們的代碼更簡單而富有可讀性。
而這裏的 koa-compose 主要目的是爲了在中間件堆棧中不斷下發執行權,轉異步調用爲同步調用。
讓咱們來舉個例子:
let one = (ctx, next) => {
console.log('middleware one execute begin');
next();
console.log('middleware one execute end');
}
let two = (ctx, next) => {
console.log('middleware two execute begin');
next();
console.log('middleware two execute after');
}
let three = (ctx, next) => {
console.log('middleware three execute begin');
next();
console.log('middleware three execute after');
}
compose([one,two,three])
複製代碼
最終打印的結果是:
middleware one execute begin
middleware two execute begin
middleware three execute begin
middleware three execute after
middleware two execute after
middleware one execute end
複製代碼
爲何會這樣呢?
仔細看 compose 函數,它首先是對 middleware自己及其元素作了類型檢查,以後就用了一個閉包來保存下標 index ,重點其實在下面這一行:
Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
fn 是一箇中間件函數,傳入的第二個參數也就是 next ,就是下一個中間件函數: middleware[i+1],經過在當前中間件函數中調用 next 咱們才能將執行權交給下一個中間件函數。
注意:這裏介紹的 koa-compose 和咱們介紹的 Koa 不一樣,是較新的版本,舊版是用生成器實現的,思想都是差很少的,只是具體實現不一樣罷了。因此你能夠看到 co 函數,它引用自 一個名爲 co 的庫,用於 Generator 函數的自動執行,這樣就不用本身寫執行器啦。
onerror()
app.onerror = function(err){
if (!this.outputErrors) return;
if (404 == err.status) return;
console.error(err.stack);
};
複製代碼
這,也沒啥好說的,y1s1,是挺簡陋的。咱們固然但願監聽函數的綁定能更加豐富一些。不過初版嘛,能理解。
加上註釋一共17行。
var http = require('http');
var codes = http.STATUS_CODES;
/** * Produce exports[STATUS] = CODE map. */
Object.keys(codes).forEach(function(code){
var n = ~~code;
var s = codes[n].toLowerCase();
exports[s] = n;
});
複製代碼
實際上,就是把 http 模塊的 STATUS_CODES key-value 關係倒置了一下,而後用 ~~
雙取反邏輯運算符將string -> number。至於爲何要用這麼 hack 的方法,我也猜不出。
最後咱們再來梳理下,我覺得Koa 與 Express 這種 web framework 不一樣之處在於 Koa 更像一個架子,它只是提供了一種中間件註冊和調用的機制。這讓 Koa 更加輕量化,具備更高的自由度,而且 Koa 使用 generator 和 co 讓其對 http 請求和響應實現了較爲優雅的攔截 。而2.0版本以後,Koa 用 async 替代了generator ,再也不須要藉助 co ,可是這也不過是跟着語言規範在調整而已, async、await 也不過是 generator 的語法糖嘛。
就簡單說到這裏啦。