koa 框架是基于 Node.js 下一代的 web server 框架, 舍弃了回调写法, 提高了错误处理效率, 而且其不绑定任何中间件, 核心代码只提供优雅轻量的函数库.
平时经常使用到 koa 框架, 所以希望通过阅读源码学习其思想, 本文是基于 koa2 的源码进行分析.
koa 整体架构
koa 框架的源码结构非常简单, 在 lib 文件夹下, 只有 4 个文件, 分别是application.js, context.js, request.js, response.js.
- application.js 是 koa 框架的入口文件;
- context.js 的作用是创建网络请求的上下文对象;
- request.js 是用于包装 koa 的 request 对象的;
- response.js则是用于包装 koa 的 response 对象的.
我们这里使用 koa 框架建立一个简单的 node 服务, 以此来逐步了解 koa 内部机理.
const koa = require('koa');
const app = new koa();
app.use(async (ctx, next) {
ctx.body = 'Hello World';
});
app.listen(3000);
上面的代码, 先生成了一个 koa 对象, 然后通过使用 use 函数往 server 中添加中间件函数, 最后使用 listen 函数进行对 3000 端口的监听.
koa 源码剖析
由上面的简单代码, 我们会有几个疑问: koa 对象中包含了些什么属性与方法? use 函数对于中间件函数的处理是怎么样的? listen 函数做了什么?
因此我们先来看一下 application.js 的源码:
application.js
application.js 暴露了一个 Application 类供我们使用, 也即是说, 我们 new 一个 koa 对象实质上就是新建一个 Application 的实例对象. 而 Application 类是继承于 EventEmitter (Node.js events 模块)的, 所以我们在 koa 实例对象上可以使用 on, emit 等方法进行事件监听.
构造函数
constructor() {
super(); // 因为继承于 EventEmitter, 这里需要调用 super
this.proxy = false; // 代理设置
this.middleware = []; // 存储中间件的list
this.subdomainOffset = 2; // 子域名偏移设置
this.env = process.env.NODE_ENV || 'development'; // node 环境变量
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
可以看到在 constructor 函数中, 实例对象会初始化几个重要的属性,
- proxy 属性是代理设置;
- middleware 属性是中间件数组, 用于存储中间件函数的;
- subdomainOffset 属性是子域名偏移量设置;
- env 属性保存 node 的环境变量 NODE_ENV 值;
- context, requets, response 则是 koa 自身的包装的 context 对象, request 对象, response 对象.
这里特别讲解一下 proxy 属性与subdomainOffset 属性. proxy 属性值是 true 或者 false, 它的作用在于是否获取真正的客户端 ip 地址(详细请看附录的第一点). subdomainOffset 属性会改变获取 subdomain 时返回数组的值, 比如 test.page.example.com 域名, 如果设置 subdomainOffset 为 2, 那么返回的数组值为 [“page”, “test”], 如果设置为 3, 那么返回数组值为 [“test”].
app.use()与中间件
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3\. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
本文基于koa2,也就是async/await版本,所以关于generator 函数暂且不看。
所以在调用app.use()时,也很简单,仅仅是把当前中间件push进中间件数组this.middleware。
所以, 所谓中间件函数的串联其实就是通过数组来逐个执行的, 至于 koa 是怎么利用 koa-compose 建立起核心的中间件机制的, 这里按下不表, 详细请阅读 理解 koa 中间件机制 博文.
listen 原理
listen 函数的原理其实很简单, 它实际上是一个缩写的函数, 它本质上就是在内部通过 Node 原生的http 模块建立起一个 http server, 而这个 http server 的回调函数使用的是 koa 中的 callback 函数的执行结果(也就是callback函数return 的函数).
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
下面我们来看一下this.callback()函数。
callback() {
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
// handleRequest 函数相当于 http.creatServer 的回调函数, 有 req, res 两个参数,
// 代表原生的 request, response 对象.
const handleRequest = (req, res) => {
// 每次接受一个新的请求就是生成一次全新的 context
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err); // 错误处理
const handleResponse = () => respond(ctx); // 响应处理
// 为 res 对象添加错误处理响应, 当 res 响应结束时, 执行 context 中的 onerror 函数
// (这里需要注意区分 context 与 koa 实例中的 onerror)
onFinished(res, onerror);
// 执行中间件数组所有函数, 并结束时调用 respond 函数
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
对于 this.createContext 函数, 它的用于就是生成一个新的 context 对象并建立 koa 中 context, requets, response 属性之间与原生 http 对象的关系的.
而 handleRequest 函数只是负责执行中间件所有的函数, 并在中间件函数执行结束的时候调用 respond.
对于在 koa 中的 context 对象, request 对象, response 对象与 http 模块原生的 req 与 res 之间的关系我并不打算陈列代码, 下面我以图解的形式来帮助阅读:
request.js
request.js主要是对原生的 http 模块的 requets 对象进行封装, 其实就是对 request 对象某些属性或方法通过重写 getter/setter 函数进行代理, 请看下面的图进行更好的理解:
内容协商
TODO
response.js
同样的, response.js 也是对 http 模块的 response 对象进行封装, 通过对 response 对象的某些属性或方法通过重写 getter/setter 函数进行代理, 请看下面的图帮助理解:
context.js
分析了上面的 request 与 response, context 的分析更为简单了, context 的核心就是通过 delegates 这一个库, 将 request, response 对象上的属性方法代理到 context 对象上.
也就是说例如 this.ctx.headersSent 相当于 this.response.headersSent. request 对象与 response 对象的所有方法与属性都能在 ctx 对象上找到. 这里我们来看一下 delegates 库的属性代理函数的片段, 借此理解一下 context 是如何代理 request 与 response 上的属性与方法的:
delegate(proto, 'response')
.getter('headerSent');
Delegator.prototype.getter = function(name){
// this.proto 指向原型, 这里的 proto 就是上面的 proto, 也就是说 context 对象
var proto = this.proto;
// target 是指 'response' 字符串
var target = this.target;
// 将 name 加入到 delegator 实例对象的 getters 数组中
this.getters.push(name);
// 调用原生的 __defineGetter__ 方法进行 getter 代理, 那么 proto[name] 就相当于 proto[target][name]
// 而 context.response 就相当于 response 对象
// 由此实现属性代理
proto.__defineGetter__(name, function(){
return this[target][name];
});
return this;
};
参考文章
koa-用到的delegates NPM包详解
koa-compose源码阅读