Koa2原理解析
Koa是继Express之后的又一大方异彩的后端框架。相比于Express其最大的变化在于支持async/await的异步流程控制以及中间件的变化。本文将按照流程对其原理进行讨论。
启动前
在Koa程序运行前存在着三个静态方法集合如下:
Request
包含着操作Node原生请求对象的一些方法,例如获取query,请求url等。
Response
包含着操作Node原生响应对象的一些方法,例如设置状态码,header等。
Context
这是Koa最重要的概念之一,如其名是上下文的意思,Request和Response自身的方法都会委托到Context中。
Context又分为两个部分,一部分是自身属性,一部分是Request和Response委托的操作方法,主要是为了给用户提供更方法获取参数的方法。源码如下:
var delegate = require('delegates');
var proto = module.exports = {}; // 一些自身方法,被我删了
/**
* Response delegation.
*/
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('set')
.method('append')
.access('status')
.access('message')
.access('body')
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable');
/**
* Request delegation.
*/
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.method('acceptsCharsets')
.method('accepts')
.method('get')
.method('is')
.access('querystring')
.access('idempotent')
.access('socket')
.access('search')
.access('method')
.access('query')
.access('path')
.access('url')
.getter('origin')
.getter('href')
.getter('subdomains')
.getter('protocol')
.getter('host')
.getter('hostname')
.getter('header')
.getter('headers')
.getter('secure')
.getter('stale')
.getter('fresh')
.getter('ips')
.getter('ip');
method是委托方法,getter委托getter,access委托getter和setter。
启动Server
Koa的启动很简单,就分为两步。第一步new一个Koa实例,第二步调用listen方法监听端口即可。看起来好像是在第一步的时候启动了服务器,实际上并不是。先看启动部分的源码:
module.exports = class Application extends Emitter {
/**
* Initialize a new `Application`.
*
* @api public
*/
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);
}
可以看到第一步并没有启动服务器,但还是初始化了一些很有用的东西。包括初始化了一个中间件集合数组,以及Request,Response,Context的创建和委托,继承Emitter。
接下来看listen方法的源码:
app.listen = function(){
debug('listen');
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
app.callback = function() {
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);
};
}
可以看到在调用监听方法时启动了一个server并立即执行了一个callback函数。这个函数内部可以分成两部分看,第一部分是执行函数时的代码,也就是初始化中间件。另一部分是接受请求后所执行的代码。
初始化中间件
通过compose函数去处理中间件数组,compose的全名叫koa-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!')
}
/**
* @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
const fn = middleware[i] || next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
首先会执行dispatch函数并传入参数0,然后会递归调用参数并依次加1,该参数对应着每次取到的中间件。取到中间件后会执行并传递context和next两个参数。
Context则是koa的上下文,next函数则是返回下一个dispatch函数的执行结果,也就是会去执行下一个中间件。可以看到每一个中间件执行结果都被一个Promise包装,并且如果需要决议掉该Promise,需要等到下一个中间件Promise决议完成。而在下一个中间件中一旦调用next又会触发新一轮的中间件连锁,以此类推,则实现了洋葱式结构。
该compose函数会返回dispatch(0),则是第一个中间件函数准备启动的状态了。下面就该接受请求并启动了。
接受请求
首先通过createContext创建一个完整版的ctx,这不仅包括了原版context的自有属性和委托属性,还挂载了原生req对象,res对象和app对象。源码如下:
app.createContext = function(req, res){
// 继承
var context = Object.create(this.context);
var request = context.request = Object.create(this.request);
var response = context.response = Object.create(this.response);
// 往context,request,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.onerror = context.onerror.bind(context);
context.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, {
keys: this.keys,
secure: request.secure
});
context.accept = request.accept = accepts(req);
context.state = {};
// 最后返回完整版context
return context;
};
然后通过onFinished函数监听response,如果发生错误就会执行ctx.onerror,用于设置response的错误信息。
onerror: function(err){
// don't do anything if there is no error.
// this allows you to pass `this.onerror`
// to node-style callbacks.
if (null == err) return;
if (!(err instanceof Error)) err = new Error('non-error thrown: ' + err);
// delegate
this.app.emit('error', err, this);
// nothing we can do here other
// than delegate to the app-level
// handler and log.
if (this.headerSent || !this.writable) {
err.headerSent = true;
return;
}
// unset all headers
this.res._headers = {};
// force text/plain
this.type = 'text';
// ENOENT support
if ('ENOENT' == err.code) err.status = 404;
// default to 500
if ('number' != typeof err.status || !statuses[err.status]) err.status = 500;
// respond
var code = statuses[err.status];
var msg = err.expose ? err.message : code;
this.status = err.status;
this.length = Buffer.byteLength(msg);
this.res.end(msg);
}
最后则是启动第一个中间件触发整个流程了,最后通过respond函数对最终的ctx做处理,来决定以什么类型响应请求。
function respond() {
// allow bypassing koa
if (false === this.respond) return;
var res = this.res;
if (res.headersSent || !this.writable) return;
var body = this.body;
var code = this.status;
// ignore body
if (statuses.empty[code]) {
// strip headers
this.body = null;
return res.end();
}
if ('HEAD' == this.method) {
if (isJSON(body)) this.length = Buffer.byteLength(JSON.stringify(body));
return res.end();
}
// status body
if (null == body) {
this.type = 'text';
body = this.message || String(code);
this.length = Buffer.byteLength(body);
return res.end(body);
}
// 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);
this.length = Buffer.byteLength(body);
res.end(body);
}
总结
整套流程大概分为3部分:
请求前——初始化中间件
使用use函数将所有中间件函数收集到middleware数组,在监听时利用compose函数将其转化为洋葱式结构
请求时——创建上下文
利用createContext函数生成最终上下文并启动第一个中间件函数触发整个流程。
请求后——处理最终上下文
利用respond函数对最终返回到客户端的请求做处理。
可以看到,代码虽少,但是非常优雅,值得仔细的学习。
-- EOF --