Koa 源码解析

1,127 阅读5分钟
原文链接: mahao.farbox.com

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,如下图所示:

enter image description here

  • 如何实现上述流程?
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())))
}
// 本人形容为:“层层包裹”

compose源码

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

co源码

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.03

    5 middleware
    8074.10

    10 middleware
    7526.55

    15 middleware
    7399.92

    20 middleware
    7055.33

    30 middleware
    6460.17

    50 middleware
    5671.98

    100 middleware
    4349.37

    总结: 挂载的中间件越多,可支持的请求数越少。通常以50个中间件为例,在1S中内能处理的请求数为:5671;一分钟内能处理的请求数为:340,260;一天即可处理4.4亿次。总的来说,性能对比的排序为:Koa2 > Koa1 > Express
    另外:对比其它框架,例如Thinkjs、Sails;就性能上来说Koa性能会更好,但Thinkjs更适合用于大型项目
    Koa-Express性能测试
    Thinkjs Koa Express Sails性能测试