回过头去看我们之前的context/request/response.js文件,就能知道当时使用的this.res或者this.response之类的是从哪里来的了,原来是在这个createContext方法中挂载到了对应的实例上。一张图来说明其中的关系:
构建了运行时上下文ctx之后,我们的app.use回调函数参数就都基于ctx了。
下面一张图描述了ctx对象的结构和继承关系:
最后回忆我们的ctx.body方法,并没有直接返回消息体,而是将消息存储在了一个变量属性中。为了每次回调函数处理结束之后返回消息,我们创建了responseBody方法,主要作用就是通过ctx.body读取存储的消息,然后调用ctx.res.end返回消息并关闭连接。从方法中知道,我们的body消息体可以是字符串,也可以是对象(会序列化为字符串返回)。注意这个方法的调用是在回调函数结束之后调用的,而我们的回调函数是一个async函数,其执行结束后会返回一个Promise对象,因此我们只需要在其后通过.then方法调用我们的responseBody即可,这就是this.callbackFunc(ctx).then(respond)的意义。
然后我们来测试一下目前为止的框架。修改example.js如下:
let simpleKoa = require('./application'); let app = new simpleKoa(); app.use(async ctx => { ctx.body = 'hello ' + ctx.query.name; }); app.listen(3000, () => { console.log('listening on 3000'); });
可以看到这个时候我们通过app.use传入的已经不再是原生的function (req, res)回调函数,而是koa2中的async函数,接收ctx作为参数。为了测试,在浏览器访问localhost:3000?name=tom,可以看到返回了'hello tom',符合预期。
这里再插入分析一个知识概念。从刚才的实现中,我们知道了this.context是我们的中间件中上下文ctx对象的原型。因此在实际开发中,我们可以将一些常用的方法挂载到this.context上面,这样,在中间件ctx中,我们也可以方便的使用这些方法了,这个概念就叫做ctx的扩展,一个例子是阿里的egg.js框架已经把这个扩展机制作为一部分,融入到了框架开发中。
下面就展示一个例子,我们写一个echoData的方法作为扩展,传入errno, data, errmsg,能够给客户端返回结构化的消息结果:
let SimpleKoa = require('./application'); let app = new SimpleKoa(); // 对ctx进行扩展 app.context.echoData = function (errno = 0, data = null, errmsg = '') { this.res.setHeader('Content-Type', 'application/json;charset=utf-8'); this.body = { errno: errno, data: data, errmsg: errmsg }; }; app.use(async ctx => { let data = { name: 'tom', age: 16, sex: 'male' } // 这里使用扩展,方便的返回utf-8格式编码,带有errno和errmsg的消息体 ctx.echoData(0, data, 'success'); }); app.listen(3000, () => { console.log('listenning on 3000'); });
主线三:中间件机制
到目前为止,我们成功封装了http server,并构造了context, request, response对象。但最重要的一条主线却还没有实现,那就是koa的中间件机制。
关于koa的中间件洋葱执行模型,koa 1中使用的是generator + co.js执行的方式,koa 2中则使用了async/await。关于koa 1中的中间件原理,我曾写过一篇文章进行解释,请移步:深入探析koa之中间件流程控制篇
这里我们实现的是基于koa 2的,因此再描述一下原理。为了便于理解,假设我们有3个async函数:
async function m1(next) { console.log('m1'); await next(); } async function m2(next) { console.log('m2'); await next(); } async function m3() { console.log('m3'); }
我们希望能够构造出一个函数,实现的效果是让三个函数依次执行。首先考虑想让m2执行完毕后,await next()去执行m3函数,那么显然,需要构造一个next函数,作用是调用m3,然后作为参数传给m2
let next1 = async function () { await m3(); } m2(next1); // 输出:m2,m3
进一步,考虑从m1开始执行,那么,m1的next参数需要是一个执行m2的函数,并且给m2传入的参数是m3,下面来模拟:
let next1 = async function () { await m3(); } let next2 = async function () { await m2(next1); } m1(next2); // 输出:m1,m2,m3
那么对于n个async函数,希望他们按顺序依次执行呢?可以看到,产生nextn的过程能够抽象为一个函数:
function createNext(middleware, oldNext) { return async function () { await middleware(oldNext); } } let next1 = createNext(m3, null); let next2 = createNext(m2, next1); let next3 = createNext(m1, next2); next3(); // 输出m1, m2, m3
进一步精简: