前言
好久没有写博客了,一方面是工作了比较忙,一方面是觉得之前的博客都是一个小点写成一篇文章,没有比较大的点,最近刚好在看 Koa 源码,就把这个写成了文章。
整体概况
Koa 很精简,总共有四个文件:application.js
、context.js
、request.js
、response.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 ;
/**
* 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
93module.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
8const 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
包,返回对象的 subdomainOffset
,proxy
,env
三个值。其中 proxy
表示是否开启代理,默认为 false
如果开启代理,对于获取 request
请求中的 host
,protocol
,ip
分别优先从 Header
字段中的 X-Forwarded-Host
,X-Forwarded-Proto
,X-Forwarded-For
获取。subdomainOffset
的意思是子域名的偏移。假设域名是”http://tobi.ferrets.example.com"。如果 app.subdomainOffset
没有设置,也就是说默认要忽略的偏移量是2,那么 ctx.subdomains
是["ferrets", "tobi"]
,如果设置为3,那么结果是 ["tobi"]
。
1 | const Koa = require('koa'); |
接下来,第三行1
2
3app.use(async ctx => {
ctx.body = 'Hello World';
});
调用了 use
方法,那我们看看 use
:1
2
3
4
5
6
7
8
9
10
11
12use(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
5listen(...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
12callback() {
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
21return (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
,然后将 Node
的 request
对象和 response
对象赋值给对应的属性,然后返回 context
给 handleRequest
使用。handleRequest
:1
2
3
4
5
6
7
8handleRequest(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
和 已经转成中间件的 fnMiddleware
,fnMiddleware
经过 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
58function 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
这个包,将 response
和 request
上面的属性、方法代理到 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
22function 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
或者定义 getter
,setter
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
/**
* 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)
}
}
}
}
源码很简单,先是判断传入的参数是否为函数数组,然后返回一个函数,接收context
,next
两个参数,然后返回dispatch
函数,这个函数才是真正的执行传入的函数。对于一个中间件多次调用 next()
会报错,举个例子看看:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21async 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
就是一个函数,接收 context
和 next
两个参数,然后开始执行。
第一次执行,index = -1
, i = 0
, index < i
不会报错,继续执行,index = i = 0
,fn = middleware[i] = middleware[0] = first
,i !== middleware.length
,fn
有定义,执行 try
的东西,Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
fn = first
即 Promise.resolve(first(context, dispatch.bind(null, i + 1)));
执行 first
,函数输出 1
,并调用第一个 next
,这里的 next
就是 dispatch.bind(null, 0 + 1)
,再次调用 dispatch
。此时 first
未执行完,只是中途跳出去执行下一个函数了,这就是洋葱模型。
第二次执行,index = 0
, i = 1
, index < i
不会报错,继续执行,index = i = 1
,fn = middleware[i] = middleware[1] = second
,i !== middleware.length
,fn
有定义,执行 try
的东西,Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
fn = second
即 Promise.resolve(second(context, dispatch.bind(null, i + 1)));
执行 second
,函数输出 2
,并调用 next
,这里的 next
就是 dispatch.bind(null, 1 + 1)
,再次调用 dispatch
。
第三次执行,index = 1
, i = 2
, index < i
不会报错,继续执行,index = i = 2
,fn = middleware[i] = middleware[2]
,i === middleware.length
,fn === next
,这个 next
就是一开始 com
传入的 hello
,Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
fn = hello
即 Promise.resolve(hello(context, dispatch.bind(null, 2 + 1)));
执行 hello
,函数输出 hello
,这个相当于洋葱模型的最中心,hello
函数没有调用 next
,执行完之后就开始返回上一个函数。
返回 second
,second
执行完毕。
返回 first
,接下来还有第二个 next()
,那就开始执行 next
。
第四次执行,由于在第一次执行的时候,next
就是 dispatch.bind(null, 0 + 1)
,再次调用 dispatch
时,index = 2
,i = 1
,此时 i <= index
,报错。这样就可以判断 next
函数调用多次。
借着这个例子,把 compose
整个流程都过了一遍,对大家理解中间件的执行有更好的帮助。
总结
Koa 短小精悍,短短几百行的核心代码,就提供了一套优雅的扩展性强的方法,值得学习学习。
上面内容如有错误,欢迎大家指出。
参考: