Koa Analysis
Abstract
这是一篇Koa框架分享的文章,主要关注Koa v1,其中包括了Koa的使用、中间件及上下文对象的大致实现原理。
Koa简介
- koa 是由 Express 原班人马打造的,致力于成为一个更小、更富有表现力、更健壮的 Web 框架。
- 使用 koa 编写 web 应用,通过组合不同的 generator,可以免除重复繁琐的回调函数嵌套,并极大地提升错误处理的效率。
- koa 不在内核方法中绑定任何中间件,它仅仅提供了一个轻量优雅的函数库,使得编写 Web 应用变得得心应手。
总结: 更小、更健壮、不绑定中间件、可避免回调金字塔
Koa 安装
Koa支持node v4+以上的版本,在使用node v0.12 时,需要用 --harmony-generators
或者 --harmony
标志,目的是为了使用ES6的新特性,例如中间件使用的Generators。
$ npm install koa
$ node --harmony app.js
总结:低版本的node需要采用和谐模式(harmony),目的是使用ES6新特性
Koa 使用
var koa = require('koa'); // 引入依赖
var app = koa(); // application实例化
app.use(function *(){
this.body = 'Hello World';
}); // 加载中间件
app.listen(3000); // 启动服务并监听3000端口
总结:通过app.use、app.listen两个函数即可实现加载中间件以及启动服务
- koa构造函数源码
// application源码片段
function Application() {
if (!(this instanceof Application)) return new Application;
this.env = process.env.NODE_ENV || 'development';
this.subdomainOffset = 2; // 定义子域名偏移,req.subdomains获取"tobi.ferrets.example.com",默认返回 ["ferrets", "tobi"]
this.middleware = []; // 存放中间件的集合
this.proxy = false;
this.context = Object.create(context); // 以context为原型创建一个新的对象。
this.request = Object.create(request); // 同上
this.response = Object.create(response); // 同上
}
- app.use 源码
var app = Application.prototype; // app为Application的原型
app.use = function(fn){
if (!this.experimental) {
// es7 async functions are not allowed,
// so we have to make sure that `fn` is a generator function
assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function');
} // 判断fn是否为GeneratorFunction
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn); // 把中间件GeneratorFunction push到this.middleware
return this; // 链式调用,返回this
};
- app.listen 源码
app.listen = function(){
debug('listen');
var server = http.createServer(this.callback());
// 启动服务,每次接收到请求就会执行this.callback(),并传入req和res两个参数
return server.listen.apply(server, arguments);
// 思考?为何为apply而不是call.
};
- this.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));
// fn = co.wrap(compose(this.middleware)),因为this.experimental通常为false
/*--------接收到请求后执行的代码--------*/
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); // 当请求完成、失败或者错误时,调用回调函数ctx.onerror(错误处理函数)
fn.call(ctx).then(function handleResponse() {
respond.call(ctx);
}).catch(ctx.onerror); // 响应对应的请求
}
};
总结:app.listen本质为http.createServer的语法糖,每次接收到请求,就会调 用this.callback(),另外可以在多个端口上启动一个 app,比如同时支持 HTTP 和 HTTPS;app.use功能仅仅是向数组中push中间件而已
Koa 中间件执行流程
Koa 中间件以一种非常传统的方式级联。
var koa = require('koa');
var app = koa();
// x-response-time 中间件
app.use(function *(next){
// 第一步:进入路由
var start = new Date;
yield next; // 遇到yield,跳到下一个中间
// 第五步: 再次进入 x-response-time 中间件,执行yield后面的代码,记录2次通过此中间件「穿越」的时间
var ms = new Date - start;
this.set('X-Response-Time', ms + 'ms');
// 第六步:返回 this.body
});
// logger 中间件
app.use(function *(next){
// 第二步:进入 logger 中间件
var start = new Date;
yield next; // 遇到yield,跳到下一个中间件
// 第四步: 再次进入 logger 中间件,执行yield后面的代码,记录2次通过此中间件「穿越」的时间
var ms = new Date - start;
console.log('%s %s - %s', this.method, this.url, ms);
});
// response 中间件
app.use(function *(){
// 第三步: 进入 response 中间件,没有yield,不再往下执行,跳回上一个中间件
this.body = 'Hello World';
});
app.listen(3000);
将中间件比作一个洋葱,Request会“穿过”洋葱,并得到返回的结果Response,如下图所示:
- 如何实现上述流程?
var fn = this.experimental
? compose_es7(this.middleware)
: co.wrap(compose(this.middleware));
1.compose是什么鬼?
// 有3个中间件
this.middlewares = [function *m1() {}, function *m2() {}, function *m3() {}];
// 通过compose转换
var midComposed = compose(this.middlewares);
// 转换后得到的middleware是这个样子的
function *() {
yield *m1(m2(m3(noop())))
}
// 本人形容为:“层层包裹”
2.co.wrap又是什么鬼?简直要崩溃!
流程控制模块,把异步变成同步的模块(瞬间逼格拉低),通过该模块,Generator函数会自动执行,并返回一个Promise实例。
co.wrap = function (fn) { createPromise.__generatorFunction__ = fn; return createPromise; function createPromise() { // co函数调用后返回promise实例 return co.call(this, fn.apply(this, arguments)); } };
Context 上下文对象
1. Koa将request和response两个对象封装到了Context对象。因此,可以通过ctx.request 和 ctx.response 去访他们。
2. request和response通过对node原生的res和req对象进行包装得到。因此,调用ctx.request.method最终调用的是node的原生req.method。
- request对象、response对象(request.js以及response.js)
get header() {
return this.req.headers; // this.req是什么鬼?在源码中并没有定义啊!!
},
set method(val) {
this.req.method = val;
},
总结:request对象,在获取属性时,会调用对应的get函数,并返回node原生对象req的同名属性,同理response对象原理
- context对象(context.js)
var delegate = require('delegates');
delegate(proto, 'request')
.method('acceptsLanguages') // method函数为方法代理
.method('acceptsEncodings')
.method('acceptsCharsets')
.method('accepts')
.method('get')
.method('is')
.access('querystring') // access为getter及setter代理
.access('idempotent')
.access('socket')
.access('search')
.access('method')
.access('query')
.access('path')
.access('url')
.getter('origin') // getter为getter代理
.getter('href')
.getter('subdomains')
.getter('protocol')
.getter('host')
.getter('hostname')
.getter('header')
.getter('headers')
.getter('secure')
.getter('stale')
.getter('fresh')
.getter('ips')
.getter('ip');
总结:通过delegate去代理response上的属性以及方法。即:通过context.query实质上获取的是request.query,最后调用req.query
delegate源码
服务器性能
-
引用官网的性能测试结果可得:
1 middleware
8367.035 middleware
8074.1010 middleware
7526.5515 middleware
7399.9220 middleware
7055.3330 middleware
6460.1750 middleware
5671.98100 middleware
4349.37总结: 挂载的中间件越多,可支持的请求数越少。通常以50个中间件为例,在1S中内能处理的请求数为:5671;一分钟内能处理的请求数为:340,260;一天即可处理4.4亿次。总的来说,性能对比的排序为:Koa2 > Koa1 > Express
另外:对比其它框架,例如Thinkjs、Sails;就性能上来说Koa性能会更好,但Thinkjs更适合用于大型项目
Koa-Express性能测试
Thinkjs Koa Express Sails性能测试