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
同理。