之前梳理Redux的时候,说到它的中间件的处理方式与koa是一样的,所以就想到那就把koa也看一遍吧,梳理一遍吧。koa非常的简洁适合阅读,该文章分析的当前版本为2.8.1。
目录结构
通过github上的package.json可以看到,koa的入口文件是lib/application.js。整个lib才四个文件,当然里面引入了一些其他的工具函数。
── lib
├── application.js
├── context.js
├── request.js
└── response.js
下面从程序入口出发,也就是application.js。
application.js
先看一下关键的相关依赖:
const response = require('./response');
const context = require('./context');
const request = require('./request');
const compose = require('koa-compose');
const http = require('http');
const Emitter = require('events');
...
可以看到,入口文件把其他三个js文件都引入进来,然后主要引入了http、koa-compose和events,其他的我先省略了。koa-compose库主要是为了对koa的中间件进行合并的工具函数,其他两个都是node的标准库。
该文件使用module.exports = class Application extends Emitter {...} 导出了koa类。这里看到Application 继承了 Emitter,所有它也包含了异步事件的处理能力,后面可以看到koa的错误处理会使用到Emitter提供的事件模型方法。
构造函数
constructor(options) {
super();
options = options || {};
this.proxy = options.proxy || false;
this.subdomainOffset = options.subdomainOffset || 2;
this.env = options.env || process.env.NODE_ENV || 'development';
if (options.keys) this.keys = options.keys;
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
重点可以看到:
- 声明和初始化了
middleware属性,是用来存放之后添加的中间件的。 - 使用
Object.create方法,分别创建了一个对象,这些对象的原型分别指向context、request和response,分别对应最开始引入的其他三个js文件。
到这里,先写一份使用koa的使用的示例代码,主要引导整个处理流程:
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx, next) => {
console.log('@@ start 1')
next()
console.log('@@ end 1')
})
app.use(async (ctx, next) => {
console.log('@@ start 2')
next()
console.log('@@ end 2')
})
app.listen(3000)
use
从上往下,当执行new Koa()的时候,也就是调用上面说的构造函数。来到app.use添加中间件的时候:
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
use函数会判断传入的中间件如果是generator,就会使用convert函数去将生成器转成类async/await函数,这个兼容会在3.*之后去掉。核心的一步就是将函数添加到了this.middleware队列里面。
listen
这里就是正式的使用http库提供的createServer方法来创建一个web服务了,并且监听相应的端口。
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
根据http.createServer的文档和使用,可以肯定这里的this.callback()函数执行会返回一个 (req, res) => {} 这样的函数。
callback
callback() {
const fn = compose(this.middleware);
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;
}
首先就将已经注册的中间件进行了合并,这里就是经典的洋葱模型中间件机制。然后判断了一下是否有过错误监听,没有的话就添加一个,这里利用了Emitter的事件模型,最后返回一个http.createServer能够使用的回调函数。
到这里,整个服务的启动流程已经结束。
处理请求
当服务收到请求,最后执行的就是传入http.createServer的回调函数。
handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
在处理请求之前,首先使用this.createContext将请求和响应对象进行了聚合封装成了ctx对象,然后再交给handleRequest函数去处理。
createContext
createContext(req, res) {
const context = Object.create(this.context);
const request = context.request = Object.create(this.request);
const response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.originalUrl = request.originalUrl = req.url;
context.state = {};
return context;
}
首先通过Object.create函数去创建了一个原型为this.context的空对象,之后就是为这个对象赋值了,可以看到在平时使用的时候访问的一些属性是怎么来的了,他们之间的关系是怎么样的可以很清楚的看见。
handleRequest
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror); // 当http的请求关闭,出现错误的时候,执行回调。
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
默认给响应状态设置为404,然后创建一个统一错误处理的回调函数和响应处理函数函数。将ctx传给被合并后的中间件,然后使用then和catch分别来处理中间件等正常处理和异常监控。
respond
当请求经过了所有的中间件处理之后,在最后调用handleResponse方法,然后去执行respond函数,最终组织响应对象,进行服务响应。
function respond(ctx) {
...
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
看到body的类型支持Buffer、string、Stream和JSON。
小结
通过梳理application.js可以知道,它核心主要是做了这样几个事情:
- 通过
http启动web服务,创建koa实例。 - 处理合并中间件为洋葱模型。
- 创建和封装高内聚的context。
- 实现异步函数的统一错误处理机制。