TIP
本文基于Koa v2.0.0。
1. Koa vs Express
Koa是继Express之后,Node的又一主流Web开发框架。相比于Express,Koa只保留了核心的中间件处理逻辑,去掉了路由,模板,以及其他一些功能。详细的比较可以参考Koa vs Express。
另一方面,在中间件的处理过程中,Koa和Express也有着一定区别,看下面例子:
// http style
http.createServer((req, res) => {
// ...
})
// express style
app.use((req, res, next) => {
// ...
})
// koa style
app.use((ctx, next) => {
// ...
})
Node自带的http
模块处理请求的时候,参数是一个req
和res
,分别为http.IncomingMessage
和http.ServerResponse
的实例。
Express对请求参数req
和res
的原型链进行了扩展,增强了req
和res
的行为。
而Koa并没有改变req
和res
,而是通过req
和res
封装了一个ctx (context)
对象,进行后面的逻辑处理。
关于Express的深入解读,可以参考之前的博文:
2. Koa基本组成
Koa源码非常精简,只有四个文件:
application.js
:Application(或Koa)负责管理中间件,以及处理请求context.js
:Context维护了一个请求的上下文环境request.js
:Request对req
做了抽象和封装response.js
:Response对res
做了抽象和封装
3. Application
Application主要维护了中间件以及其它一些环境:
// application.js
module.exports = class Application extends Emitter {
constructor() {
super();
this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
// ...
}
通过app.use(fn)
可以将fn
添加到中间件列表this.middleware
中。
app.listen
方法源码如下:
// application.js
listen() {
debug('listen');
const server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
}
首先会通过this.callback
方法来返回一个函数作为http.createServer
的回调函数,然后进行监听。我们已经知道,http.createServer
的回调函数接收两个参数:req
和res
,下面来看this.callback
的实现:
// application.js
callback() {
const fn = compose(this.middleware);
if (!this.listeners('error').length) this.on('error', this.onerror);
return (req, res) => {
res.statusCode = 404;
const ctx = this.createContext(req, res);
onFinished(res, ctx.onerror);
fn(ctx).then(() => respond(ctx)).catch(ctx.onerror);
};
}
首先是将所有的中间件通过compose
组合成一个函数fn
,然后返回http.createServer
所需要的回调函数。于是我们可以看到,当服务器收到一个请求的时候,会使用req
和res
通过this.createContext
方法来创建一个上下文环境ctx
,然后使用fn
来进行中间件的逻辑处理。
4. Context
通过上面的分析,我们已经可以大概得知Koa处理请求的过程:当请求到来的时候,会通过req
和res
来创建一个context (ctx)
,然后执行中间件。
事实上,在创建context
的时候,还会同时创建request
和response
,通过下图可以比较直观地看到所有这些对象之间的关系。
图中:
- 最左边一列表示每个文件的导出对象
- 中间一列表示每个Koa应用及其维护的属性
- 右边两列表示对应每个请求所维护的一些对象
- 黑色的线表示实例化
- 红色的线表示原型链
- 蓝色的线表示属性
实际上,ctx
主要的功能是代理request
和response
的功能,提供了对request
和response
对象的便捷访问能力。在源码中,我们可以看到:
// context.js
delegate(proto, 'response')
.method('attachment')
// ...
.access('status')
// ...
.getter('writable');
delegate(proto, 'request')
.method('acceptsLanguages')
// ...
.access('querystring')
// ...
.getter('ip');
这里使用了delegates模块来实现属性访问的代理。简单来说,通过delegate(proto, 'response')
,当访问proto
的代理属性的时候,实际上是在访问proto.response
的对应属性。
5. Request & Response
Request对req
进行了抽象和封装,其中对于请求的url相关的处理如图:
┌────────────────────────────────────────────────────────┐
│ href │
├────────────────────────────┬───────────────────────────┤
│ origin │ url / originalurl │
├──────────┬─────────────────┼──────────┬────────────────┤
│ protocol │ host │ path │ search │
├──────────├──────────┬──────┼──────────┼─┬──────────────┤
│ │ hostname │ port │ │?│ querystring │
│ ├──────────┼──────┤ ├─┼──────────────┤
│ │ │ │ │ │ │
" http: │ host.com : 8080 /p/a/t/h ? query=string │
│ │ │ │ │ │ │
└──────────┴──────────┴──────┴──────────┴─┴──────────────┘
Response对res
进行了封装和抽象,这里不做赘述。
6. 中间件的执行
在上面已经提到,所有的中间件会经过compose处理,返回一个新的函数。该模块源码如下:
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!')
}
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'))
// 当前执行第 i 个中间件
index = i
let fn = middleware[i]
// 所有的中间件执行完毕
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
// 执行当前的中间件
// 这里的fn也就是app.use(fn)中的fn
return Promise.resolve(fn(context, function next() {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
Koa的中间件支持普通函数,返回一个Promise的函数,以及async函数。由于generator函数中间件在新的版本中将不再支持,因此不建议使用。