Maples7's Blog

Koa2 源码赏析

随着 Node 新版本开始支持 async/await 异步控制写法,Koa 也相继发布了它的 2.0 版本。用 async/await 写法的 Koa 来开发项目,Node 开发者再也没有任何理由不「拥抱变化」——从 Express 转到 Koa 上来。实际上,对于普通 Node 开发者——Express 框架的用户——而言,从 Express 转到 Koa 没有任何技术壁垒,当然前提是你至少得知道 ES2017 中 async/await 是个什么东西。

图片显示错误

前言

有了 Express 的积累,以 TJ 为首的 Express 开发团队对于 Koa 的设计也更加的得心应手、游刃有余。从源代码来看,Koa 看起来甚至比 Express 更加简洁和灵活,然而在功能上却丝毫没有让步,甚至更胜一筹。一方面,这得益于 JavaScript 越来越方便好用的各种「语法糖」;另一方面,也在于 Koa 本身简洁强大的设计:它不再绑定任何特定的中间件,也去掉了其他任何多余的设计(连路由系统都抽象成了第三方中间件),而只是简单的提供了一个优雅管理各种中间件的约束系统,用户的所有挂载都是中间件。

TJ 大神的代码一向简洁强大,Koa 的源码也是如此。如果你去 GitHub 上查看 Koa 的源码,你同样会被其简洁所震撼,核心代码不过 4 个文件,平均每个文件代码行数也就四百来行,看似简简单单,却天才般的把 Express 线性的中间件控制流转变为「洋葱体」结构,从而解锁了更多的姿势和玩法。本文就直接深入 Koa 的源码(v2.2.0),来欣赏 Koa 的曼妙身姿。

代码大体结构

lib 目录下总共就四个文件:application.jscontext.jsrequest.jsresponse.js。入口文件是 application.js,导出的是一个继承了 Node 内建模块 Events 的 Class,构造函数中进行了必要的参数初始化,并且把 contextrequestresponse 属性指向了原型链指向其他三个文件导出对象的实例。

然后是类方法,主要的几个 public 的方法如下:

  • listen:一个简单的对 http.createServer(this.callback()).listen(...) 的封装。
  • callback:在 listen 中有调用,返回一个用于 http.createServer 的回调函数 handleRequest,在这个函数中创建了主角 ctx,并做了一些原型链继承和 aliases。更重要的是调用的 koa-compose 返回了一个 fn 函数,负责了整个中间件「洋葱体」流程的实现和控制。在所有中间件执行完之后,做了一些返回之前的琐碎诸如设置必要的返回头等的工作。
  • use:把中间件参数放入 this.middleware 数组,并返回 this 以便链式调用。

结合 Koa 文档和 application.js,基本就可以对整个框架的处理流程有个整体的把握了。其中最主要的部分还是 callback 函数中的内容,看完之后对整个基于 Node HTTP 模块封装的中间件处理的流转过程都清楚了。

Context

这里是对 this.context 的原型对象的实现。

没有太多值得一提的东西,基本是对上下文对象 ctx 提供几个必需的原型接口以及一个缺省的错误处理函数 onerror。有意思的是,利用 delegates 包,把对 app 的一些属性的访问直接对应的代理到 responserequest 上,这也就是文档上所说的 Request aliasesResponse aliases 的具体原因

Request & Response

这俩文件是对 this.requestthis.response 的原型对象的实现。

this.requestthis.response 中都有大量属性的 getter 和 setter 方法,这些可用的属性在 Koa 文档中都已经列出,代码在这里对它们的读写操作进行了实现。这些属性基本是对 Node HTTP 包中 req 和 res 属性的封装。

「洋葱体」带来了什么

你固然还是可以像在 Express 中一样把 next(); 都放在每个中间件的末尾来线性的传递控制权,但「回形针」式的控制流带来了更多的可能。一个最典型的的例子就是 response-time 中间件的实现。

在 GitHub 上用 response-time 关键字搜索,前两个 Repo 就分别是 Express 和 Koa 中对这个中间件的实现。

Express 中的实现其实是 hack 了被 Express 用到的 Node.js 内部 HTTP 模块的 res.writeHead 方法(实际实现细节在 response-time 中间件调用的 on-headers 包中),使得在这一层注入了一小段代码用于在数据返回前计算时间差并写入 Response Headers。这样虽然可以实现,但显然不够好。它 hack 了框架底层的一个内部方法,虽然也巧妙,但代码本身并不是在给它天生就安排好的合适的地方来实现的,可以视为晦涩的「奇技淫巧」,而且与 Express 的内部实现强耦合,指不定哪天 Express 改了 res.writeHead 调用时机,这个中间件的返回值可能就有所不同了(当然按实际情况来说 Express 此处应该也不会改了,而且 Express 的 response-time 最初也是 TJ 写的)。

Koa 的 response-time 实现自不必多说,官方文档上就有,来欣赏它的简洁优雅:

1
2
3
4
5
6
app.use(async function(ctx, next) {
const start = new Date();
await next();
const ms = new Date() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});

总结来说,Koa 的「洋葱体」结构使得每个中间件能够在同一次请求的前后对称的部分提供相同的上下文环境,这样就让实现像 response-time 这样的中间件变得相当简单。

推荐阅读

听说,你想请我吃糖?0.0