koa 的核心源码只有 lib
目录下的四个文件,极其精简,毕竟是个 small ~570 SLOC codebase。
koa 入口指向 main": "lib/application.js"
,首先看 lib/application.js
能看到 Application 类继承自 nodejs 的核心模块 events,所以使用时的 app 实例也就具备了 app.emit
事件触发与 app.on
事件监听功能,事实上 node 中大部分模块的实现也都继承自events 模块。展看 Application 类看一下:
看到两个个常用的方法:注册中间件的 use、开启并监听服务的 listen,还有一个不常用的 callback。
use
use
的实现,1、兼容koa1 的 generator 写法。2、维护一个中间件数组:
listen
listen
的实现,1、http.createServer。2、把 this.callback() 的返回结果作为createServer 的 requestlistener 参数 :
callback
所以再看 callback
的实现:
callback
返回一个响应请求的 handleRequest
函数。这个函数首先会用 createContext
构造一个 请求的 context 对象,这也是 koa 的核心概念。跳出去看下 createContext
方法, 调用了 lib 目录下另外三个文件创建了 context、response、request 并做了一系列引用绑定,后面再看。
看下 handleRequest
函数,做的事就是先调用封装好的中间件 fnMiddleware
,再返回对 response 的处理 respond(ctx)
。所以在 koa 中,所有的这些封装好的中间件 fnMiddleware
,会在 handleRequest
之前执行。
koa-compose
fnMiddleware
是在 callback
中用 koa-compose 封装的: const fn = compose(this.middleware);
, 看一下 koa-compose:
只有这么多行的一个函数。参数是给定的中间件数组,数组里是中间件函数 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
比如有两个中间件:
执行顺序如下:
- 在
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:
接下来看 lib/request.js
,是对 request 属性的一系列getter 和 setter。有些注细节值得注意:
ips 获取请求经过所有的中间设备 IP 地址数组, app.proxy 为 true 才有效,数组内容从 client ip 到最远端 ip
接下来看 lib/response.js
,也是对 response 属性的一系列getter 和 setter。
有些注细节值得注意:
当 response.body
没设置或为空时,koa会自动把 response.status
设为 204 https://github.com/koajs/koa/pull/1493