前言

好久没有写博客了,一方面是工作了比较忙,一方面是觉得之前的博客都是一个小点写成一篇文章,没有比较大的点,最近刚好在看 Koa 源码,就把这个写成了文章。

整体概况

Koa 很精简,总共有四个文件:application.jscontext.jsrequest.jsresponse.js,其他一些工具函数都写成一个包了,方便其他人使用。

application.js

application.js 是整个 Koa 的入口文件,我们从这个文件开始,解析 Koa 源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
'use strict';

/**
* Module dependencies.
*/

const isGeneratorFunction = require('is-generator-function'); // 用来判断一个函数是否 generator
const debug = require('debug')('koa:application'); // 引入 debug 包,并且为 debug 设置输出的命名空间
const onFinished = require('on-finished'); // 当一个 http 请求 closes, finishes, 或者 errors 时,调用回调函数
const response = require('./response'); // 引入 response 文件
const compose = require('koa-compose'); // compose 主要是将中间件组合起来,形成 ‘洋葱模型’
const isJSON = require('koa-is-json'); // 判断一个 response 的 body 是否是 json
const context = require('./context'); // 引入 context 文件
const request = require('./request'); // 引入 request 文件
const statuses = require('statuses'); // 判断 http 的状态码
const Emitter = require('events'); // node 的 events 提供了一个订阅发布模式
const util = require('util'); // node 的 util 包
const Stream = require('stream'); // node 的 stream 处理流数据的抽象接口
const http = require('http'); // node 的 http 包
const only = require('only'); // only 函数返回对象的指定键值
const convert = require('koa-convert'); // 将 generator 函数转成 promise 函数
const deprecate = require('depd')('koa'); // 给出标记为废弃的信息

一开始用了 use strict,开启严格模式,具体的作用可以看这篇文章
)
然后就是一堆引入,这里打了注释,告诉你这个包是用来干嘛的。

接下来我们来看大概:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
module.exports = class Application extends Emitter {
/**
* Initialize a new `Application`.
*
* @api public
*/

constructor() {...}

/**
* Shorthand for:
*
* http.createServer(app.callback()).listen(...)
*
* @param {Mixed} ...
* @return {Server}
* @api public
*/

listen(...args) {...}

/**
* Return JSON representation.
* We only bother showing settings.
*
* @return {Object}
* @api public
*/

toJSON() {...}

/**
* Inspect implementation.
*
* @return {Object}
* @api public
*/

inspect() {...}

/**
* Use the given middleware `fn`.
*
* Old-style middleware will be converted.
*
* @param {Function} fn
* @return {Application} self
* @api public
*/

use(fn) {...}

/**
* Return a request handler callback
* for node's native http server.
*
* @return {Function}
* @api public
*/

callback() {...}

/**
* Handle request in callback.
*
* @api private
*/

handleRequest(ctx, fnMiddleware) {...}

/**
* Initialize a new context.
*
* @api private
*/

createContext(req, res) {...}

/**
* Default error handler.
*
* @param {Error} err
* @api private
*/

onerror(err) {...}
};

/**
* Response helper.
*/

function respond(ctx) {...}

可以看到第一行

1
module.exports = class Application extends Emitter

继承了 Emitter 这个包,就是用了 node 的发布订阅模式,一般用于监听 error 事件。
现在我们通过官网一个最简单的例子来解析源码:

1
2
3
4
5
6
7
8
const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
ctx.body = 'Hello World';
});

app.listen(3000);

首先第一步引入 Koa,然后就是 const app = new Koa(),这一步会创建一个 Koa 实例,执行 constructor 方法。
让我们看看 constructor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Initialize a new `Application`.
*
* @api public
*/
constructor() {
super();

this.proxy = false; // 这个是给 request.js 用的
this.middleware = []; // 中间件数组
this.subdomainOffset = 2; // 这个是给 request.js 用的
this.env = process.env.NODE_ENV || 'development'; // 环境
this.context = Object.create(context); // 创建 context 对象
this.request = Object.create(request); // 创建 request 对象
this.response = Object.create(response); // 创建 response 对象
if (util.inspect.custom) {
// 这里自定义 util 包的 inspect 方法
this[util.inspect.custom] = this.inspect;
}
}

既然看到 inspect 方法了,那我们就看看具体:

1
2
3
4
5
6
7
8
9
10
/**
* Inspect implementation.
*
* @return {Object}
* @api public
*/

inspect() {
return this.toJSON();
}

inspect 就是调用了 toJson 方法,看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Return JSON representation.
* We only bother showing settings.
*
* @return {Object}
* @api public
*/

toJSON() {
return only(this, [
'subdomainOffset',
'proxy',
'env'
]);
}

就是用到了一开始引用的 only 包,返回对象的 subdomainOffsetproxyenv 三个值。其中 proxy 表示是否开启代理,默认为 false 如果开启代理,对于获取 request 请求中的 hostprotocolip 分别优先从 Header 字段中的 X-Forwarded-HostX-Forwarded-ProtoX-Forwarded-For 获取。subdomainOffset的意思是子域名的偏移。假设域名是”http://tobi.ferrets.example.com"。如果 app.subdomainOffset 没有设置,也就是说默认要忽略的偏移量是2,那么 ctx.subdomains["ferrets", "tobi"],如果设置为3,那么结果是 ["tobi"]

1
2
3
4
5
6
7
8
const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
ctx.body = 'Hello World';
});

app.listen(3000);

接下来,第三行

1
2
3
app.use(async ctx => {
ctx.body = 'Hello World';
});

调用了 use 方法,那我们看看 use

1
2
3
4
5
6
7
8
9
10
11
12
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;
}

首先判断参数 fn 是否为函数,不是就报错。然后判断 fn 是否为 generator 函数,是的话就输出即将要废弃的提示,接着把 fn,用引入的 convert 包,将 generator 转成 promise。然后输出 debug 信息,最后 push 到中间件数组 middleware,返回 this

最后的一句话

1
app.listen(3000);

用到了 listen 方法,看看:

1
2
3
4
5
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}

输出 debug 信息,然后调用 http 包的 createServer 方法,传入 callback 函数,最后调用 server.listen,传入 args 参数,启动 HTTP 服务器监听连接。其中调用了 callback,这个 callback 方法是 application.js 的核心:

1
2
3
4
5
6
7
8
9
10
11
12
callback() {
const fn = compose(this.middleware);

if (!this.listenerCount('error')) this.on('error', this.onerror);

const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};

return handleRequest;
}

这个函数也很简单,就是调用 compose 方法,把 middleware 组合起来,compose 这个方法还是很重要的,下面会讲到,先跳过。然后判断一下有没有监听 error 这个事件,没有就监听,用了 onerror 方法 ,接下来返回一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
return (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
```
这个函数首先调用 `createContext` 方法,把参数 `req`、`res`传进去:
```js
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
return context;
}

就是每次请求都会创建一个 context,然后将 Noderequest 对象和 response 对象赋值给对应的属性,然后返回 contexthandleRequest 使用。
handleRequest

1
2
3
4
5
6
7
8
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

这个函数的作用就是执行中间件,执行完之后调用 respond 方法。首先接收上面的 context 和 已经转成中间件的 fnMiddlewarefnMiddleware 经过 compose 处理之后其实是长这样:

1
fnMiddleware = function (context, next) {...}

然后设置默认的 statusCode 为 404,然后调用 onFinished,监听 res 结束,执行完所有中间件的时候,调用 respond,这个 respond 方法并没有在 Application 类里面,而是外面的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
function respond(ctx) {
// allow bypassing koa
if (false === ctx.respond) return;
// 检查是否可写
if (!ctx.writable) return;

const res = ctx.res;
let body = ctx.body;
const code = ctx.status;

// ignore body
// 用 statuses 包判断 code 是否为 body 为空的类型,如 204,205 等
if (statuses.empty[code]) {
// strip headers
// 将 body 置空
ctx.body = null;
return res.end();
}

// 如果 method 是 HEAD 类型
if ('HEAD' == ctx.method) {
// headersSent 表示响应头是否被发送出去了
// 如果头部没有被发送,就设置 length
if (!res.headersSent && isJSON(body)) {
ctx.length = Buffer.byteLength(JSON.stringify(body));
}
return res.end();
}

// status body
if (null == body) {
// 判断 Http 协议的版本
if (ctx.req.httpVersionMajor >= 2) {
body = String(code);
} else {
// ctx.message 在这里其实是 ctx.req.message,Http 的 req,res 都有 message 字端
body = ctx.message || String(code);
}
if (!res.headersSent) {
ctx.type = 'text';
ctx.length = Buffer.byteLength(body);
}
return res.end(body);
}

// responses
// 根据 body 不同的类型做不同的处理
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);

// body: json
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}

context.js

这个文件主要是用 delegates 这个包,将 responserequest 上面的属性、方法代理到 context 上,比如:this.response. headerSent 就等于 this.ctx.headerSent
举个例子:delegate(proto, 'response').getter('headerSent')

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Delegator(proto, target) {
if (!(this instanceof Delegator)) return new Delegator(proto, target);
this.proto = proto;
this.target = target;
this.methods = [];
this.getters = [];
this.setters = [];
this.fluents = [];
}

Delegator.prototype.getter = function(name){
var proto = this.proto;
var target = this.target;
this.getters.push(name);

proto.__defineGetter__(name, function(){
return this[target][name];
});

return this;
};
...

源码很简单,用了 proto.__defineGetter__ 这个被废弃的方法,现在一般都用 Object. defineProperty 或者定义 gettersetter

request.js

这里引用网上一张图

response.js

这里也引用网上一张图

compose

这个方法可以算是核心了,把中间件组合起来形成洋葱模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
'use strict'

/**
* Expose compositor.
*/

module.exports = compose

/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/

function compose (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!')
}

/**
* @param {Object} context
* @return {Promise}
* @api public
*/

return function (context, next) {
// last called middleware #
// 这里 index 是正在执行的中间件的下标
let index = -1
return dispatch(0)
function dispatch (i) {
// 判断中间件的 next 函数调用多次
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// 获取中间件
let fn = middleware[i]
// 如果已经到最后一个中间件,那么最后执行就是传入的 next 函数
if (i === middleware.length) fn = next
// 如果函数没有定义,那么直接 resolve
if (!fn) return Promise.resolve()
try {
// 执行中间件
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}

源码很简单,先是判断传入的参数是否为函数数组,然后返回一个函数,接收contextnext 两个参数,然后返回dispatch 函数,这个函数才是真正的执行传入的函数。对于一个中间件多次调用 next() 会报错,举个例子看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
async function first(ctx, next) {
console.log('1');
await next();
await next(); // 两次调用 next
console.log(ctx);
};

async function second(ctx, next) {
console.log('2');
await next();
};

const middleware = [first, second];

const com = compose(middleware);

const hello = function() {
console.log('hello');
}

com('ctx', hello);

compose 把中间件串起来,结合上面的源码可知,com 就是一个函数,接收 contextnext 两个参数,然后开始执行。
第一次执行,index = -1i = 0index < i 不会报错,继续执行,index = i = 0fn = middleware[i] = middleware[0] = firsti !== middleware.lengthfn 有定义,执行 try 的东西,Promise.resolve(fn(context, dispatch.bind(null, i + 1))); fn = firstPromise.resolve(first(context, dispatch.bind(null, i + 1))); 执行 first,函数输出 1,并调用第一个 next,这里的 next 就是 dispatch.bind(null, 0 + 1),再次调用 dispatch。此时 first 未执行完,只是中途跳出去执行下一个函数了,这就是洋葱模型。

第二次执行,index = 0i = 1index < i 不会报错,继续执行,index = i = 1fn = middleware[i] = middleware[1] = secondi !== middleware.lengthfn 有定义,执行 try 的东西,Promise.resolve(fn(context, dispatch.bind(null, i + 1))); fn = secondPromise.resolve(second(context, dispatch.bind(null, i + 1))); 执行 second,函数输出 2,并调用 next,这里的 next 就是 dispatch.bind(null, 1 + 1),再次调用 dispatch

第三次执行,index = 1i = 2index < i 不会报错,继续执行,index = i = 2fn = middleware[i] = middleware[2]i === middleware.lengthfn === next,这个 next 就是一开始 com 传入的 helloPromise.resolve(fn(context, dispatch.bind(null, i + 1))); fn = helloPromise.resolve(hello(context, dispatch.bind(null, 2 + 1)));执行 hello,函数输出 hello,这个相当于洋葱模型的最中心,hello 函数没有调用 next,执行完之后就开始返回上一个函数。

返回 secondsecond 执行完毕。

返回 first,接下来还有第二个 next(),那就开始执行 next

第四次执行,由于在第一次执行的时候,next 就是 dispatch.bind(null, 0 + 1),再次调用 dispatch 时,index = 2i = 1,此时 i <= index,报错。这样就可以判断 next 函数调用多次。

借着这个例子,把 compose 整个流程都过了一遍,对大家理解中间件的执行有更好的帮助。

总结

Koa 短小精悍,短短几百行的核心代码,就提供了一套优雅的扩展性强的方法,值得学习学习。
上面内容如有错误,欢迎大家指出。

参考:

koa

koa 解析