Koa2源码阅读整理

在学习Koa2的过程中,发现Koa2的源码只有4个文件,四个文件的合起来的代码都没超过2000行,就决定把Koa2的源码看一遍,这里也做一个整理.

洋葱模型

Koa2被称为第二代Node web框架,最大的特点就是洋葱模型。

其实洋葱模型的原理很简单,就是使用ES6的async awite函数。
这里,使用async函数打印各个数字,即可明白洋葱模型的简单原理。

async function test1(params) {
  console.log(1)
  await test2()
  console.log(2)
}
async function test2(params) {
  console.log(3)
}
test1();
console.log(4)

执行结果为1 3 4 2

Koa2和Express中间件的差别

这里以官方获取响应时间的示例代码为例,看看Koa2和Express中间件在使用上的差别。

async function(ctx, next){
    var start = Date.now();
    await next();
    var delta = Math.ceil(Date.now() - start);
    ctx.set('X-Response-Time', delta + 'ms');
}
......
var onHeaders = require('on-headers')
function responseTime (req, res, next) {
    var startAt = process.hrtime()
    onHeaders(res, function onHeaders () {
      var diff = process.hrtime(startAt)
      var time = diff[0] * 1e3 + diff[1] * 1e-6
        fn(req, res, time)
    })
    next()
  }
......  

如果koa中next返回的不是Promise,那么响应时间永远都是0,因为next调用完就执行下面的代码了,那么express 中间件要想做到记录响应时间必须要通过事件监听res的结束响应才可以达到同样的效果。

源码主线

阅读源码,我习惯先找出框架的主线。
从Git上下载Koa2.js的源码后,打开四个源码文件:

  • application.js
  • context.js
  • request.js
  • response.js

从package.json中可以看到:

"main": "lib/application.js",

很容易可以看出application.js是整个源码项目的入口文件.

application.js中的语法糖解析

整个模块导出的是Application类。类中包含了Koa2中文档所描述的各种语法糖。这些语法糖都是对 Node.js功能的扩展封装。这里只简单说明下Application中比较重要的语法糖,没有做全部介绍。代码都比较优雅好理解,可以自己查阅。

Application类

class Application extends Emitter{
  //......
}

整个Application都继承Node.js的events模块.所有的异步 I/O 操作在完成时都会发送一个事件到事件队列.
Emitter 的核心就是事件触发与事件监听器功能的封装.

构造函数

constructor() {
    super();
    this.proxy = false;
    this.middleware = [];
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';
    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;
    }
}

从构造函数中,就可以知道通过new Koa()中新建一个app.
可以在app中进行设置的app.env, app.proxy等默认设置都在这构造函数中
util.inspect() 方法返回 object 的字符串表示,主要用于调试。
对象可以定义自己的 [util.inspect.custom](depth, opts)(或已废弃的 inspect(depth, opts)) 函数,util.inspect() 会调用并使用查看对象时的结果:

const util = require('util');
const obj = { 
    foo: '这个不会出现在 inspect() 的输出中' 
};
obj[util.inspect.custom] = (depth) => {
  return 'Box< true >';
};
util.inspect(obj);
// 返回: Box< true >

use函数

从use函数中可以看出,Koa2.js每新增加一个中间件,都会添加到this.middleware数组中。

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

callback函数

callback() {
      const fn = compose(this.middleware);

      if (!this.listenerCount('error')) this.on('error', this.onerror);

      const handleRequest = (req, res) => {
            res.statusCode = 404;
            const ctx = this.createContext(req, res);
            const onerror = err => ctx.onerror(err);
            const handleResponse = () => respond(ctx);
            onFinished(res, onerror);
            return fn(ctx).then(handleResponse).catch(onerror);
      };
      return handleRequest;
}

在Koa2中,app.callback()是返回一个适合 http.createServer()方法的回调函数用来处理请求。
emitter.listenerCount(eventName) 返回正在监听名为 eventName 的事件的监听器的数量。

callback函数中的compose()则是出自koa-compose模块中的代码。compose()是Koa2.js的中间件核心调用机制。
compose源代码如下:

function compose (middleware) {
    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)
      }
    }
  }
}

compose函数实际上就是dispatch的递归调用。这个compose()返回一个Promise对象一致。
callback函数中创建了ctx,ctx代理了request和response的功能,提供更加快捷方便的访问。
这里就不在过多解释
context.js, request.js, response.js这三个文件,这里就不再做详细解释。
context.js中的Proto模块主要使用了delegates模块的代理request和response的功能。
而request.js和response.js则是对原始Node.js中request和response的封装和扩展。

delegate委托

ctx能够直接使用ctx.request和ctx.response中的方法,是因为源码中使用了delegate委托方法。
在context.js中即可查看到相关代码:

delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  .method('is')
  .access('querystring')
  .access('idempotent')
  .access('socket')
  .access('search')
  .access('method')
  .access('query')
...

delegate的作用就是将内部子对象的变量或者函数暴露在外层的父对象名称,方便父对象能够使用。其内部是使用Object.defineProperty()设置属性和值。

设计思想

看懂Koa2的源码后,理解Koa2中间件在洋葱模型的原理后,还需要想想,自己设计一个轻量的Web框架需要做什么?
我们先看看Koa2相比原生http模块新增哪些基础功能:

  • 中间件模式
  • 集成Cookie,这还包括普通cookie, 密钥cookie,JSON cookie
  • 提供默认的错误处理,避免暴露服务端的错误详情,并和404错误区分开来。
  • 支持各种http状态码和响应
  • 快速设置URL,querystring,path
  • ......

这些构成Koa2的基础功能。如果想要在此实现一个新的后端框架,我们还需要做哪些?

  • 增加配置项
  • 防御csrf等攻击
  • 增加model层,用于获取数据库的数据
  • 支持JSON Web Tokens
  • 支持HTTP2
  • 路由
  • 错误日志记录
  • 定时任务
  • 系统、内存监控报警
  • ......