原本打算写算法课设的,这学期课程过重,实在没办法更新技术博客,但仍是心里惭愧,偶然看到 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 的语法糖嘛。
就简单说到这里啦。