koa是Express团队打造的新一代web框架,特点是更小,更舒服的开发体验。
前两节我们已经介绍了koa的基本使用和koa项目的最佳实践,今天我们来深究下koa2的原理。
查看koa2的源码,可以发现其实现代码非常简单,只有四个js文件。
下面先从这四个js文件介绍源码的大概结构:
是koa2的入口文件,在当中有Koa实例的构造函数,该构造函数继承events,来实现对(错误)事件的触发和监听。
listen函数,是对http.createServer的封装。
listen (...args) {
debug('listen')
const server = http.createServer(this.callback())
return server.listen(...args)
}
use函数,收集中间件。
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
}
callback函数,是用于处理中间件,安排中间件的执行顺序,并返回http.createServer可以处理的回调函数。
/**
* Return a request handler callback
* for node's native http server.
*
* @return {Function}
* @api public
*/
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
}
/**
* Handle request in callback.
*
* @api private
*/
handleRequest (ctx, fnMiddleware) {
const res = ctx.res
res.statusCode = 404
const onerror = err => ctx.onerror(err)
const handleResponse = () => respond(ctx)
onFinished(res, onerror)
return fnMiddleware(ctx).then(handleResponse).catch(onerror)
}
就是中间件参数中的ctx,上下文,最主要的功能是基于delegates模块实现的。
delegates 基本用法就是将内部对象的变量或者函数绑定在暴露在外层的变量上。
通过delegates把ctx.repsponse.status等等repsponse和request上的属性,暴露在ctx上,类似ctx.status。
const proto = module.exports = {
//...
}
delegate(proto, 'response')
.method('attachment')
.access('status')
//...
这两个类是对原生req和res的封装(这个原生req和res是http.createServer的回调函数返回的),用get和set对外暴露了很多方便使用的属性和方法,我们ctx访问的repsponse和request上的属性,其实是这些get和set方法。
/**
* Get response status code.
*
* @return {Number}
* @api public
*/
get status () {
return this.res.statusCode
},
/**
* Set response status code.
*
* @param {Number} code
* @api public
*/
set status (code) {
if (this.headerSent) return
assert(Number.isInteger(code), 'status code must be a number')
assert(code >= 100 && code <= 999, `invalid status code: ${code}`)
this._explicitStatus = true
this.res.statusCode = code
if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code]
if (this.body && statuses.empty[code]) this.body = null
},
下面重点介绍中间件和洋葱模型执行顺序实现。
首先我们要了解中间件的执行顺序,先看下面这段代码。
let Koa = require('koa');
let app = new Koa();
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(6);
});
app.use(async (ctx, next) => {
console.log(2);
await next();
console.log(5);
});
app.use(async (ctx, next) => {
console.log(3);
ctx.body = "hello world";
console.log(4);
});
app.listen(3000, () => {
console.log('listenning on 3000');
});
输出的顺序是123456,koa的中间件是按洋葱模型的顺序执行的。
中间件之间通过 next 函数联系,当一个中间件调用 next()
后,会将控制权交给下一个中间件,直到下一个中间件不再执行 next()
时沿路返回,依次将控制权交给上一个中间件。
那么,怎么实现这种执行顺序呢?
上面初看代码的时候,我们已经知道,在use
中通过this.middleware.push(fn)
完成了中间件的搜集,然后在callback
中处理中间件的执行顺序。
我们先回顾一下koa的application.js。
/**
* Return a request handler callback
* for node's native http server.
*
* @return {Function}
* @api public
*/
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
}
/**
* Handle request in callback.
*
* @api private
*/
handleRequest (ctx, fnMiddleware) {
const res = ctx.res
res.statusCode = 404
const onerror = err => ctx.onerror(err)
const handleResponse = () => respond(ctx)
onFinished(res, onerror)
return fnMiddleware(ctx).then(handleResponse).catch(onerror)
}
再看一下koa-compose
中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) {
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)
}
}
}
}
我们先删掉不重要的部分,整理下代码,只保留实现洋葱模型执行顺序的代码。
function compose (middleware) {
return function (context, next) {
return dispatch(0)
function dispatch (i) {
let fn = middleware[i]
if (i === middleware.length) fn = next // 最后一个中间件也处理完,fn指向next
if (!fn) {
return Promise.resolve() // fn为null直接resolve
} else {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
}
}
}
}
重点在fn(context, dispatch.bind(null, i + 1))
,在resolve
中,调用了fn
,在fn
中又调用了dispatch(i+1)
。
我们可以先理解fn实现了下面这样的接口。
function fn(ctx, next){
return next()
}
用前面打印顺序的例子,我们来盘一下执行顺序。
dispatch(0)
,fn指向第一个中间件,在resolve
中执行,然后就 console.log(1);
。next()
,于是调用到dispatch(1)
,fn指向第二个中间件,执行fn,console.log(2)
。next()
,于是调用到dispatch(2)
,fn指向第三个中间件,执行fn,console.log(3)
,继续console.log(4)
。console.log(5)
。(函数调用栈的原理)console.log(6)
。OK了,顺序这就搞清楚了!
了解了koa2的源码,最直观的一个感受就是,koa2的实现方式很先进,而且很简洁。
大量使用了es6的新特性,和一些功能强大又小巧的第三方模块,最终的koa2的产品,也遵从这种简洁的设计理念,只做好一个中间件框架,不附带一点点其他更多的功能。