前置工作
上Github找到相应的Koa版本,下载过来并解压
笔者用的是2.13.1版本
初窥全貌
浏览Koa的package.json文件,发现
{
"main": "lib/application.js",
"exports": {
".": {
"require": "./lib/application.js",
"import": "./dist/koa.mjs"
}
}
}
由此不难看出,lib文件夹下的application.js正是Koa的主体所在。此外,Koa的源码全部在lib文件夹下,而lib文件夹十分简洁,只有四个文件
- application.js Koa主体
- context.js Koa上下文
- request.js 封装request
- response.js 封装response
总的来看application.js的代码,能发现Koa的实现其实并不复杂。其核心部分如下
const Emitter = require('events')
module.exports = class Application extends Emitter {
constructor(options) {
super()
// initalize...
}
// 起一个服务器
listen(...args) { }
// 注册middleware
use(fn) { }
}
是的,就是这么简单,和我们使用Koa一样简单!
整个Koa应用是一个基于Node里面的事件触发器的类。它在Node中有着相当重要的地位,我们看看官网是怎么说的
Node.js 的大部分核心 API 都是围绕惯用的异步事件驱动架构构建的,在该架构中,某些类型的对象(称为"触发器")触发命名事件,使
Function对象("监听器")被调用。例如:
net.Server对象在每次有连接时触发事件;fs.ReadStream在打开文件时触发事件;流在每当有数据可供读取时触发事件。所有触发事件的对象都是
EventEmitter类的实例。这些对象暴露了eventEmitter.on()函数,允许将一个或多个函数绑定到对象触发的命名事件。
以下示例展示了使用单个监听器的简单的
EventEmitter实例。eventEmitter.on()方法用于注册监听器,eventEmitter.emit()方法用于触发事件。const EventEmitter = require('node:events'); class MyEmitter extends EventEmitter {} const myEmitter = new MyEmitter(); myEmitter.on('event', () => { console.log('an event occurred!'); }); myEmitter.emit('event');
这种经典的.on式的回调写法,在Node中无处不在,如http.Server等。可以说EventEmitter是Node异步IO机制的基石。
中间件模式
提到Koa,就免不了要提到其中间件模式。它正是Koa设计上的精髓所在。请求到了服务器,依次按序被注册的中间件所处理。其相关实现并不复杂
class Application extends Emitter {
constructor(options) {
super()
this.middleware = [] // 存放中间件
// ...
}
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
debug('use %s', fn._name || fn.name || '-')
this.middleware.push(fn)
return this // 提供链式调用的能力
}
}
处理中间件模式的Server结构是这样的
const compose = require('koa-compose')
const onFinished = require('on-finished')
class Application extends Emitter {
listen (...args) {
debug('listen')
const server = http.createServer(this.callback()) // 通过原生的http.createServer创建
return server.listen(...args)
}
// 为Node的原生http server返回一个request handler
callback() {
// 将所有的中间件组合成一个函数,该函数返回一个Promise
const fn = compose(this.middleware)
// 如果没有注册error回调,则注册默认的onerror
if (!this.listenerCount('error')) this.on('error', this.onerror)
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
return handleRequest
}
handleRequest (ctx, fnMiddleware) {
const res = ctx.res
res.statusCode = 404
const onerror = err => ctx.onerror(err)
const handleResponse = () => respond(ctx)
// on-finished包的作用是
// Execute a callback when a HTTP request closes, finishes, or errors.
// 这行代码会在res出错时,执行onerror函数
onFinished(res, onerror)
// ctx会作为最初的context,传给中间件
return fnMiddleware(ctx).then(handleResponse).catch(onerror)
}
// 根据请求与响应,创建上下文
createContext(req, res) {
}
//默认error handler
onerror(err) {
}
}
// response helper
function respond(ctx) {
//...
}
koa-compose
接上,我们先来看koa-compose这个工具,它用于将中间件整合起来。其源码只有短短48行,相当精简。直接贴上附带注释的源码
'use strict'
/**
* Expose compositor.
*/
module.exports = compose
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}
其核心就在于dispatch函数。该函数依次取出middleware中的函数,并将其通过Promise.resolve()串联在一起。只要middleware中的函数执行了next(),下一个函数也将紧跟着执行,直到遍历完整个middleware数组。得益于Event Loop,Promise.resolve()使所有异步函数依次在微任务队列里执行完。这里还用index指向上一个被调用的middleware function,所以出现了闭包结构。
Settings
Koa还支持设置实例的一些属性,如app.env,app.keys等。
这个就比较简单了,其相关实现如下
/**
*
* @param {object} [options] Application options
* @param {string} [options.env='development'] Environment
* @param {string[]} [options.keys] Signed cookie keys
* @param {boolean} [options.proxy] Trust proxy headers
* @param {number} [options.subdomainOffset] Subdomain offset
* @param {string} [options.proxyIpHeader] Proxy IP header, defaults to X-Forwarded-For
* @param {number} [options.maxIpsCount] Max IPs read from proxy IP header, default to 0 (means infinity)
*
*/
constructor (options) {
super()
options = options || {}
this.proxy = options.proxy || false
this.subdomainOffset = options.subdomainOffset || 2
this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For'
this.maxIpsCount = options.maxIpsCount || 0
this.env = options.env || process.env.NODE_ENV || 'development'
if (options.keys) this.keys = options.keys
}
request,response,context
在Node的原生http模块中,http.createServer接受requestListener回调函数作为其参数。该函数的两个参数request和response,是分别基于http.IncomingMessage类和http.ServerResponse类的。在Koa中,为了简化、方便开发者对其的处理,Koa自己封装了request和response,可以理解为对原生IncomingMessage和ServerResponse的一层抽象。
context则是将request和response对象封装成一个对象,为开发提供了很多有用的属性与API
lib文件夹的三个相应的文件,正是书写了这三者的Prototype,在Koa主体中以Object.create(proto)的方式使用。
context.js中还利用了delegates这个年久失修的包,凭借委托的设计模式来控制context上的request和response的行为。
例如
const delegate = require('delegates')
const proto = module.exports = { }
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
其源代码里居然还出现了用proto.__defineGetter__来改写[[ Get ]]行为的写法。17年有位老哥提了个用Object.defineProperty代替的PR,也没人管...
详细内容,读者若有兴趣可自行查阅,文档与源码照着一起看。
小结
这应该是笔者首次尝试去阅读一个开源项目的源码,受益良多。希望日后能不断地阅读优秀源码,不断变强