Koa 源码阅读

koa 的核心源码只有 lib 目录下的四个文件,极其精简,毕竟是个 small ~570 SLOC codebase。
image.png
koa 入口指向 main": "lib/application.js" ,首先看 lib/application.js

Javascript
const Emitter = require('events');
module.exports = class Application extends Emitter {
 ...

能看到 Application 类继承自 nodejs 的核心模块 events,所以使用时的 app 实例也就具备了 app.emit 事件触发与 app.on 事件监听功能,事实上 node 中大部分模块的实现也都继承自events 模块。展看 Application 类看一下:
image.png
看到两个个常用的方法:注册中间件的 use、开启并监听服务的 listen,还有一个不常用的 callback。

use

use  的实现,1、兼容koa1 的 generator 写法。2、维护一个中间件数组:

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

listen

listen  的实现,1、http.createServer。2、把 this.callback() 的返回结果作为createServer 的 requestlistener 参数 :

Javascript
listen(...args) {
  debug('listen');
  const server = http.createServer(this.callback());
  return server.listen(...args);
}

callback

所以再看 callback 的实现:

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

callback  返回一个响应请求的 handleRequest  函数。这个函数首先会用 createContext构造一个 请求的 context 对象,这也是 koa 的核心概念。跳出去看下 createContext 方法, 调用了 lib 目录下另外三个文件创建了 context、response、request 并做了一系列引用绑定,后面再看。
看下 handleRequest  函数,做的事就是先调用封装好的中间件 fnMiddleware ,再返回对 response 的处理 respond(ctx) 。所以在 koa 中,所有的这些封装好的中间件 fnMiddleware ,会在 handleRequest 之前执行。

Javascript
handleRequest(ctx, fnMiddleware) {
  const res = ctx.res;
  res.statusCode = 404;
  const onerror = err => ctx.onerror(err);
  const handleResponse = () => respond(ctx);
  onFinished(res, onerror);
  return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

koa-compose

fnMiddleware 是在 callback 中用 koa-compose 封装的: const fn = compose(this.middleware); , 看一下 koa-compose:

Javascript
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!')
  }

  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 // 同步执行到哪一步,保证同一个中间件中的 next 不会执行多次
      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)
      }
    }
  }
}

只有这么多行的一个函数。参数是给定的中间件数组,数组里是中间件函数 fn 。
这里的核心是一个 dispatch 函数,koa-compose 通过递归调用 dispatch 实现遍历中间件数组。
dispatch 接受参数 i 标识当前执行到第几个中间件。执行当前的中间件 fn 时,会把下一个中间件作为 next 参数传给当前的中间件 fn 。传参的方法是通过 dispatch.bind(null, i + 1) , 所以下个中间件不会立即执行,而是在当前中间件 fn 执行到 await next() 时执行。当然如果中间件没有调用 next() , 即没有调用dispatch.bind(null, i + 1) , 那么他之后的中间件都不会执行。

所以当存在一系列中间件,koa 会从第一个中间件开始往下游执行,直到最后一个中间件执行完毕,最后一个中间件最先返回,再依次回流到第一个中间件。形成所谓的 洋葱模型。

如果执行到最后一个中间件,那么参数 next 就是所有中间件完成后最终需要执行的 handleResponse 

比如有两个中间件:

Javascript
app.use(async (ctx, next) => {
    console.log(1)
    await next();
    console.log(4)
})

app.use(async (ctx, next) => {
  console.log(2)
  await next();
  console.log(3)
})

执行顺序如下:

  • dispatch(0)中执行了第一个中间件。
  • 第一个中间件执行时传入了第二个中间件的执行方法dispatch.bind(null, i + 1)
  • 在第一个中间件中,执行第二个中间件await next()
  • 因为awati语法,需要等待第二个中间件执行完成。
  • 第二个中间件执行时传入了第三个中间件的执行方法dispatch.bind(null, i + 1)
  • 在第二个中间件中,执行第三个中间件await next()
  • 第三个中间件不存在,直接返回了Promise.resolve()
  • 第二个中间件执行await next()后面的内容,第二个中间件执行完毕。
  • 第一个中间件执行await next()后面的内容,第一个中间件执行完毕。

这就是 koa 中间件洋葱模型的核心原理了。

response && request

还剩下一个 方法 respond, 做的是处理 context、和 response 的返回值, 判断了 body 的 几种情况:string\stream\json\buffer。

接下来看 lib/context.js ,提供了 cookies 的 setter 和 getter ,一个 onerror 方法(也就是请求时处理请求错误 fnMiddleware(ctx).then(handleResponse).catch(onerror) 的 onerror ),其他主要做的事就是代理了 response 和 request:

Javascript
delegate(proto, 'response')
  .method('attachment')
  ...
delegate(proto, 'request')
  .method('acceptsLanguages')
  ...

接下来看 lib/request.js ,是对 request 属性的一系列getter 和 setter。有些注细节值得注意:
ips 获取请求经过所有的中间设备 IP 地址数组, app.proxy 为 true 才有效,数组内容从 client ip 到最远端 ip

Javascript
get ips() {
  const proxy = this.app.proxy;
  const val = this.get(this.app.proxyIpHeader);
  let ips = proxy && val
  ? val.split(/\s*,\s*/)
  : [];
  if (this.app.maxIpsCount > 0) {
    ips = ips.slice(-this.app.maxIpsCount);
  }
  return ips;
},

接下来看 lib/response.js ,也是对 response 属性的一系列getter 和 setter。
有些注细节值得注意:
response.body 没设置或为空时,koa会自动把 response.status  设为 204 https://github.com/koajs/koa/pull/1493

Javascript
set body(val) {
    const original = this._body;
    this._body = val;

    // no content
    if (null == val) {
      if (!statuses.empty[this.status]) this.status = 204;
      if (val === null) this._explicitNullBody = true;
      this.remove('Content-Type');
      this.remove('Content-Length');
      this.remove('Transfer-Encoding');
      return;
    }

    // set the status
    if (!this._explicitStatus) this.status = 200;

  	...
  },