Koa是基于 Node.js 平台的下一代 web 开发框架,它的源码可以看这里,本章通过源码来简绍一下Koa是怎么实现的。
核心代码
Koa的核心代码只有4个文件,如图。

各个文件的作用:
application.js:Koa的核心,对应Koa App类。context.js:对应上下文对象ctx。request.js:对应ctx.request对象。response.js:对应ctx.response对象。
Koa实现
Koa使用
Koa使用如下:
1 | const Koa = require('koa'); |
Koa底层是基于原生http模块,原生http模块怎么启动一个服务呢?如下:
1 | const http = require('http'); |
观察上面的代码,两者是不是挺像的。
application源码
为了方便查看application的核心逻辑,下面是我去掉了部分非核心代码的application源码:
1 | const onFinished = require('on-finished') |
当调用app.use的时候,实际上是把中间件函数加入到this.middleware数组当中。
当调用app.listen的时候,通过http.createServer来创建http服务并使用server.listen来监听服务。
这里比较难理解的是callback函数,它使用compose将中间件合并成一个调用函数,具体怎么合并的我们稍后再说。如果error事件没有监听的话,添加一个默认的监听函数,默认的onerror函数实际上就是打印错误信息;this.listenerCount是从哪里来的呢?实际上Application类是继承自node中的Emitter,该方法也是Emitter的方法。最后返回了一个handleRequest函数,该函数做了2件事,首先通过req和res构建ctx,然后调用this.handleRequest,注意this.handleRequest是Application类的属性而不是callback中的handleRequest,也就是这里并没有递归调用。
在this.handleRequest函数中调用了中间件函数fnMiddleware(ctx),当中间件函数都调用完了以后调用respond(ctx),respond通过不同的情况去处理res的结果;失败的时候调用ctx.onerror(err)。另外在中间件处理之前会调用onFinished(res, onerror)来监听出错的情况,onFinished的代码请看这里。
koa-compose源码
在讲述源码之前我们先看看koa-compose中间件是怎么使用的。
1 | const Koa = require('koa'); |
客户端打印:
1 | 第1个中间件开始 |
这就是Koa中间件著名的洋葱模型。

我们先不谈Koa只看看koa-compose做了什么事。
1 | const compose = require('koa-compose'); |
上面打印:
1 | 第1个中间件开始 |
koa-compose把多个中间件合并成一个函数,通过await next()来调用下一个中间件,其源码如下:
1 | function compose (middleware) { |
首先对middleware做类型检查,middleware必须是数组,同时每一个中间件必须是函数。然后返回一个函数,这个函数第一个参数是上下文对象,第二个参数是下个中间件执行的next函数。核心逻辑是上面的dispatch方法,在dispatch方法中会返回Promise。dispatch方法实际上就是next方法,首次会调用dispatch(0)来触发第一个中间件函数。当一个中间件中调用next方法后会把index标记为当前的索引,如果一个中间件多次调用next方法,那么由于第一次调用是index会标记为i,那么第二次调用的时候i和index是相等的,也就是第二次的时候会走if (i <= index) return Promise.reject(new Error('next() called multiple times'))逻辑,也就是会报错。每次调用的时候根据索引获取当前要执行的中间件函数,在第18行会执行当前中间件,并把下一个dispatch当作第二个参数next传入到下一个中间件中。当执行到最后一个中间件的时候,设置fn = next由于Application代码的第52行并没有传递第二个参数,所以此时next是undefined,那么compose中将会走第16行if (!fn) return Promise.resolve()的逻辑。如果传递了函数那么会执行传入的函数,当此函数中调用next以后,由于索引已经超过了middleware的长度,所以下次函数执行事也会走第16行的逻辑。
context源码
context是对上下文对象的封装,具体代码如下:
1 | const util = require('util') |
可见context实际上就是一个对象,它对Cookie和onerror做了一个封装。最后使用delegate()来代理request和response对象,delegate不了解的同学可以看下面这个示例。
1 | const delegate = require('delegates'); |
上面代理了obj对象的aaa属性,所以直接可以通过obj来访问aaa中代理的属性和方法,其中method表示代理方法,getter表示代理get方法,setter表示代理set方法,access表示不但代理了get同时也代理了set。delegates的实现也不难:
1 | function Delegator(proto, target) { |
request与response
request与response就是一个简单的对象,没什么好说的,比如request代码大致如下:
1 | module.exports = { |
这里需要注意的是有一个this.req对象,这个对象是从哪里来的?请看Application的createContext方法的第61行,在这里把node的req挂载了上来,res同理。