koa源码分析

koa源码非常简单,只有四个文件也就是koa的四大对象:

  • application.js 包含 app 的构造以及启动一个服务器
  • context.js app 的 context 对象, 传入中间件的上下文对象
  • request.js app 的请求对象,包含请求相关的一些属性
  • response.js app 的响应对象,包含响应相关的一些属性

application主要方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
application中的构造函数:

constructor() {
super();

this.proxy = false;
// 存放中间件的数组
this.middleware = [];
// 忽略的子域名数量
this.subdomainOffset = 2;
// 设置环境变量
this.env = process.env.NODE_ENV || 'development';
// 挂载context,request,response到application
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;
}
}

application除了构造方法外还有几个主要的方法,包括: applicaton的createContext方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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上下文对象,这个对象挂载了app,req,res,ctx等多个属性,属性的含义看缩写即可明白。 application中的listen方法

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

调用了基础网络库http中的createServer和linsten方法,创建一个服务器并且监听端口。

koa中间件源码原理

application中的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;
}

因为中间件是一个用于拦截请求的异步函数,所以首先判断中间件类型是否正确,并且在中间件是generator函数的情况下转为异步函数,然后把推入中间件数组并且返回本身。该步骤仅仅作为注册中间件的功能,中间件数组中的中间件将会在http发生时被依次调用。调用的过程如下:

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;
}

这边中间件的执行用了一个koa-compose的库,koa-compose也非常精简,只要一个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
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 #
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)
}
}
}
}

前几行做类型判断,不再赘述。 主要内容是返回一个函数,这个函数是一个递归的高阶函数。 最关键的一行是return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); 这就会在promise resolve的时候执行fn(context, dispatch.bind(null, i + 1)) 这个过程可以被理解为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function compose(){
return Promise.resolve(
f1(context,function(){
return Promise.resolve(
f2(context,function(){
return Promise.resolve(
f3(context,function(){
return Promise.resolve(
next(context,function(){
return Promise.resolve('fn is undefined')
})
)
})
)
})
)
})
)
}

在一个中间件函数中不能调用两次next(),否则会抛出错误。 为什么执行顺序是1,2,3,4,5,ok,5,4,3,2,1呢。是因为 next() 是把主线程暂时交给下个代码块,所有代码块执行完后会依次收回执行权,而收回的顺序就相反了。

context主要方法

相比application,剩下三部分都要简单的多。 Koa处理请求的过程:当请求到来的时候,会通过req和res来创建一个context (ctx),然后执行中间件,然后再返回响应。 context.js是用来代理ctx的功能,把更多的方法和功能挂载上去,提供对request和response的更多操作。其中最主要的方法是delegate方法,除此之外的方法都是辅助方法或者错误捕获。

1
2
3
4
5
6
7
8
9
delegate(proto, 'response')
.method('attachment')
// ...
.getter('writable');

delegate(proto, 'request')
.method('acceptsLanguages')
// ...
.getter('ip');

这段操作使得,当访问proto的代理属性的时候,实际上是在访问proto.response的对应属性。 #request和response 两者类似,虽然代码比较长,但实际就是对HTTP的header的一个处理而已,每个方法都非常简单,也调用了一些基础的网络库如url,net等等。