这两天看了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
本质上也是一个普通的对象,他们的代码都不是很难,在这里就不一一赘述了。