这两天看了koa的源码,惊叹于它的简练,仅仅聚焦最核心的功能,其他全部以中间件的形式扩展出去,给了开发者最大的个性化定制。这篇文章用于记录源码的学习笔记,方便日后借鉴思想时能快速回忆起来。
基础用法
参考官网给的示范:
| 1 | const Koa = require('koa'); | 
跑起来后在浏览器输入localhost:3000就能看到返回Hello World了。
入口
首先从构造函数开始:
| 1 | class Application extends Emitter { | 
以上就是new Koa()会执行的所有逻辑了,仅仅是一些变量的初始化,关于context、request、response这三个对象在后面会说到。
listen
注意new Koa并没有启动Server,那么显然只能在listen中启动了。
| 1 | listen(...args) { | 
看看http.createServer的函数签名http.createServer([options][, requestListener])就大致能猜到this.callback()返回的是options或requestListener。
| 1 | // Return a request handler callback for node's native http server. | 
所以我们最终传给http.createServer的是
| 1 | (req, res) => { | 
也就是 每次都先创建一个context对象,然后调用this.handleRequest。
同时我们遇到了koa的最核心的库:koa-compose。 它用于精心组合所有middleware,并按照期望的顺序调用。我们后面会用专门的章节来描述它。
先看看createContext和handleRequest的实现。
createContext
用于创建一个context对象。
| 1 | createContext(req, res) { | 
很简单,就是创建各种对象,然后各种赋值绕的很。注意request.req、response.req指向的是http模块原生的IncomingMessage对象,而request.response、response.request指向的都是koa封装后的对象。
handleRequest
这个函数用于真正的进行业务逻辑处理了。
| 1 | // fnMiddleware: 经koa-compose包装后的函数 | 
fnMiddleware是经koa-compose包装后的函数,函数签名是(context, next) => Promise, 内部会依次调用每个中间件,不管是同步还是异步的中间件。在处理完所有中间件逻辑后,Promise会resolve或reject。
onFinished是一个帮助库,用于在请求close、finish、error时执行传入的回调。
respond函数用于将中间件处理后的结果通过res.end返回客户端:
| 1 | // Response helper. | 
middleware 处理流程
终于到koa最核心的逻辑了,可以想象middleware是koa得以流行的关键所在,各式各样的中间件使得框架异常灵活,非常方便定制。 从整体上看,middleware的处理类似于DOM事件处理,先从前往后,再从后往前。
上面也说到所有中间件会传给koa-compose,并返回一个签名为(context, next) => Promise的函数。我们来仔细分析一下:
| 1 | /** | 
在koa框架中,当我们执行fnMiddleware(ctx)时,就会开始执行dispatch(0),然后开始不断递归。这里需要仔细琢磨的是这两句:
| 1 | if (i === middleware.length) fn = next; | 
当i === middleware.length成立时,实际上所有传入的middleware已经执行完,那么fn = next意味着什么呢?
其实我们调用fnMiddleware可以传入两个参数的,第二个可选参数表示最终的回调函数。例如:
| 1 | fnMiddleware(ctx, () => { | 
这个时候我们的fn = next表示fn被赋值给了这个传入的最终回调。接下来判断如果没有传入最终回调,那么整个中间件执行流程就到此结束。
另外,细细体会每个回调的执行顺序,可以发现middleware的处理类似于DOM事件处理,先从前往后,再从后往前,并且middleware可以是异步函数,因为middleware的执行被包裹在了Promise.resolve中。例如:
| 1 | const Koa = require('koa'); | 
请求localhost:3000会打印出形如:
| 1 | GET / - 4ms | 
在当前中间件中调用next时,会将控制权交给下一个中间件,当下一个中间件执行完毕时,才会执行当前中间件的next之后逻辑。
context、request、reponse
request、reponse都是对原生res、req的封装,context本质上也是一个普通的对象,他们的代码都不是很难,在这里就不一一赘述了。