koa源码解读
SunShinewyf opened this issue · 0 comments
继上次对express
进行简单地了解和深入之后,又开始倒腾koa
了。对于koa
的印象是极好的,简洁而有表现力。和express
相比它有几个比较明显的特征:
- 比较新潮,
koa1
中使用了generator
,拥抱es6
语法,使用同步语法来避免callback hell
的层层嵌套。在koa2
中又拥抱了es7
的async-await
语法,语法表现形式更为简洁。 - 变得更轻量化,相比于
express
,koa
抽离了原先内置的中间件,一些比较重要的中间件都使用单独的中间件插件,使得开发者可以根据自己的实际需要使用中间件,更加灵活。就比如给开发者建造了一个简单的地基,之后的装修设计都由开发者自己决定,精简灵活。
关于更多更详细的两者以及和hapi
的比较,读者可以移步这里
在源码方面,koa
变得更加轻量化,但是还是很有特点的。目录结构如下:
- lib/
- application.js
- context.js
- request.js
- response.js
从目录结构来看,只有四个文件,摒弃了express
中的路由模块,显得简单而有表现力。四个文件中分别定义了四个对象,分别是app
对象,context
,request
以及response
。深入源码查看,你会发现更加简单,每个文件的代码行数也是很少,而且逻辑嵌套并不复杂。
中间件原理
首先定义了一个构造函数,源码如下:
function Application() {
if (!(this instanceof Application)) return new Application;
this.env = process.env.NODE_ENV || 'development';
this.subdomainOffset = 2;
this.middleware = [];
this.proxy = false;
this.context = Object.create(context); //koa的上下文对象
this.request = Object.create(request); //koa.request
this.response = Object.create(response); //koa.request
}
在这段里面只是单纯地定义了一个实例化app
的一些属性。如上下文,中间件数组等。
然后是注册了一些中间件,中间件的use
源码很简单,就是将当前的中间件函数push
到该应用实例的middleware
数组中,在此不再赘述。
最后定义了开启服务的lisen
函数,在这个函数里面没有什么特殊的,只是有一点需要注意:它将自身原型的一个callback
作为参数传入:
var server = http.createServer(this.callback());
这一句很关键,它表示每次在开启koa
服务的时候,就会执行传入的callback
,而callback
都干了些啥,具体看源码:
app.callback = function(){
if (this.experimental) {
console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')
}
var fn = this.experimental
? compose_es7(this.middleware)
: co.wrap(compose(this.middleware));
var self = this;
if (!this.listeners('error').length) this.on('error', this.onerror);
return function handleRequest(req, res){
res.statusCode = 404;
var ctx = self.createContext(req, res);
onFinished(res, ctx.onerror);
fn.call(ctx).then(function handleResponse() {
respond.call(ctx);
}).catch(ctx.onerror);
}
};
在这个函数里面,通过experimental
参数作为是否使用es7
的async
的标准,当然了,这种折中处理的方式是koa1
中的,在koa2
中,由于完全摒弃了generator
,转而拥抱async-await
,所以直接使用的const fn = compose(this.middleware);
就简单进行处理了。对于使用es7
语法的情况,使用的compose_es7
对app
中的中间件数组进行处理:
function compose(middleware) {
return function (next) {
next = next || new Wrap(noop);
var i = middleware.length;
while (i--) next = new Wrap(middleware[i], this, next);
return next
}
}
也就是将中间件进行遍历,compose
函数的作用如下:
compose([f1,f2,...,fn])(args) =====> f1(f2(f3(..(fn(args)))));
也就是将数组里面的函数依次执行,通过一个next中间值不断将执行权进行传递。如果传入的中间件数组不是generator
函数,那么应该是依次执行,但是generator
有暂停执行的功能,所以一旦执行yield next
的时候,就会去执行下一个函数。等下一个中间件执行完成时,再在原来中断的地方继续执行。这种执行方式导致形成了koa
中著名的洋葱模型。
举例子如下:
first step before
second step before
second step after
first step after
当使用es7
语法时,处理也是一样的。
上下文context
对象
相比在express
中,koa
多了一个上下文对象,创建上下文的源码如下:
app.createContext = function(req, res){
var context = Object.create(this.context);
var request = context.request = Object.create(this.request);
var 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.onerror = context.onerror.bind(context);
context.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, {
keys: this.keys,
secure: request.secure
});
context.accept = request.accept = accepts(req);
context.state = {};
return context;
};
从中可以看出,ctx.req
、ctx.res
代表的是node
的request
和response
对象,而ctx.request
和ctx.response
则代表的是koa
的对应对象。在express
中,获取一些参数是通过访问传入的res
或者req
的对应参数。但是在koa
中,则是访问的ctx
上下文里面的相应参数。只是两者的封装不一样而已。
以上是我的一些分析,不对的地方希望大神交流和指出。