本文基于Koa v2.0.0

1. Koa vs Express

Koa是继Express之后,Node的又一主流Web开发框架。相比于Express,Koa只保留了核心的中间件处理逻辑,去掉了路由,模板,以及其他一些功能。详细的比较可以参考Koa vs Express

另一方面,在中间件的处理过程中,Koa和Express也有着一定区别,看下面例子:

// http style
http.createServer((req, res) => {
  // ...
})

// express style
app.use((req, res, next) => {
  // ...
})

// koa style
app.use((ctx, next) => {
  // ...
})

Node自带的http模块处理请求的时候,参数是一个reqres,分别为http.IncomingMessagehttp.ServerResponse的实例。

Express对请求参数reqres的原型链进行了扩展,增强了reqres的行为。

而Koa并没有改变reqres,而是通过reqres封装了一个ctx (context)对象,进行后面的逻辑处理。

关于Express的深入解读,可以参考之前的博文:

2. Koa基本组成

Koa源码非常精简,只有四个文件:

  • application.js:Application(或Koa)负责管理中间件,以及处理请求
  • context.js:Context维护了一个请求的上下文环境
  • request.js:Request对req做了抽象和封装
  • response.js:Response对res做了抽象和封装

3. Application

Application主要维护了中间件以及其它一些环境:

// application.js
module.exports = class Application extends 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);
  }

  // ...
}

通过app.use(fn)可以将fn添加到中间件列表this.middleware中。

app.listen方法源码如下:

// application.js
listen() {
  debug('listen');
  const server = http.createServer(this.callback());
  return server.listen.apply(server, arguments);
}

首先会通过this.callback方法来返回一个函数作为http.createServer的回调函数,然后进行监听。我们已经知道,http.createServer的回调函数接收两个参数:reqres,下面来看this.callback的实现:

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

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

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

首先是将所有的中间件通过compose组合成一个函数fn,然后返回http.createServer所需要的回调函数。于是我们可以看到,当服务器收到一个请求的时候,会使用reqres通过this.createContext方法来创建一个上下文环境ctx,然后使用fn来进行中间件的逻辑处理。

4. Context

通过上面的分析,我们已经可以大概得知Koa处理请求的过程:当请求到来的时候,会通过reqres来创建一个context (ctx),然后执行中间件。

事实上,在创建context的时候,还会同时创建requestresponse,通过下图可以比较直观地看到所有这些对象之间的关系。

图中:

  • 最左边一列表示每个文件的导出对象
  • 中间一列表示每个Koa应用及其维护的属性
  • 右边两列表示对应每个请求所维护的一些对象
  • 黑色的线表示实例化
  • 红色的线表示原型链
  • 蓝色的线表示属性

实际上,ctx主要的功能是代理requestresponse的功能,提供了对requestresponse对象的便捷访问能力。在源码中,我们可以看到:

// context.js
delegate(proto, 'response')
  .method('attachment')
  // ...
  .access('status')
  // ...
  .getter('writable');

delegate(proto, 'request')
  .method('acceptsLanguages')
  // ...
  .access('querystring')
  // ...
  .getter('ip');

这里使用了delegates模块来实现属性访问的代理。简单来说,通过delegate(proto, 'response'),当访问proto的代理属性的时候,实际上是在访问proto.response的对应属性。

5. Request & Response

Request对req进行了抽象和封装,其中对于请求的url相关的处理如图:

┌────────────────────────────────────────────────────────┐
│                           href                         │
├────────────────────────────┬───────────────────────────┤
│          origin            │     url / originalurl     │
├──────────┬─────────────────┼──────────┬────────────────┤
│ protocol │      host       │   path   │     search     │
├──────────├──────────┬──────┼──────────┼─┬──────────────┤
│          │ hostname │ port │          │?│ querystring  │
│          ├──────────┼──────┤          ├─┼──────────────┤
│          │          │      │          │ │              │
"  http:   │ host.com : 8080   /p/a/t/h  ?  query=string │
│          │          │      │          │ │              │
└──────────┴──────────┴──────┴──────────┴─┴──────────────┘

Response对res进行了封装和抽象,这里不做赘述。

6. 中间件的执行

在上面已经提到,所有的中间件会经过compose处理,返回一个新的函数。该模块源码如下:

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'))
      // 当前执行第 i 个中间件
      index = i
      let fn = middleware[i]
      // 所有的中间件执行完毕
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()

      try {
        // 执行当前的中间件
        // 这里的fn也就是app.use(fn)中的fn
        return Promise.resolve(fn(context, function next() {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

Koa的中间件支持普通函数,返回一个Promise的函数,以及async函数。由于generator函数中间件在新的版本中将不再支持,因此不建议使用。