本来打算写算法课设的,这学期课程太重,实在没办法更新技术博客,但还是内心惭愧,偶然看到 deno 群有人谈到Koa、nest.js、express 比对 ,就没忍住去看了看 Koa 源码,写了篇水文。作者水平有限(实际上我还没用过Koa 嘞,只是跑去看了官网的 Api),欢迎多多指错,然后我去写课设了~
首先要说明的是我参见的源码版本是 Koa 的第一个发布版 (0.0.2)。文件结构很简单,只有三个文件:application.js、context.js、status.js,下面“依次”来谈。
Context
我们还是先来看 Context 比较好。
Koa Context 将 node 的
request和response对象封装到单个对象中,为编写 Web 应用程序和 API 提供了许多有用的方法。 这些操作在 HTTP 服务器开发中频繁使用,它们被添加到此级别而不是更高级别的框架,这将强制中间件重新实现此通用功能。
Module dependencies
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() 。
Application
Application 是Koa 的入口文件。
Module dependencies
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');
有意思的构造函数
构造函数的第一行很有意思:
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,是挺简陋的。我们当然希望监听函数的绑定能更加丰富一些。不过第一版嘛,能理解。
Status
加上注释一共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 的方法,我也猜不出。
The End
最后我们再来梳理下,我以为Koa 与 Express 这种 web framework 不同之处在于 Koa 更像一个架子,它只是提供了一种中间件注册和调用的机制。这让 Koa 更加轻量化,具有更高的自由度,并且 Koa 使用 generator 和 co 让其对 http 请求和响应实现了较为优雅的拦截 。而2.0版本之后,Koa 用 async 替代了generator ,不再需要借助 co ,但是这也不过是跟着语言规范在调整而已, async、await 也不过是 generator 的语法糖嘛。
就简单说到这里啦。