前言
上一篇《nodejs之express中间件》已经对express中间件的实现做了详细的讲解,同时也对实现中间件的框架Connect的源码做了简单的分析。并且也提到了express的中间件是直线型,koa的中间件是洋葱型。本篇就来说说koa的中间件。
koa介绍
koa和express是同一个团队开发的。与express很像,也是一个自身功能极简的框架,所以在一个项目中所需要的东西大多是以中间件的形式引入。
目前koa有1.x和2.x版本,1.x版本基于generator,2.x版本基于
generator/async、await。由于generator的语法相比async又很明显的劣势,所以后续的版本中会去掉generator的使用,而是全部采用async的方式。不过在2.x这个过渡版中依然兼容generator,需要注意的是在使用generator或者使用了依赖generator的第三方库时,会报出一个警告,大致意思“generator在当前版本还可以正常使用,但是会在后续的版本中移除”。
注:本文所有koa的写法都是koa2版本的。并且兼容koa3。
koa中间件
相比express的直线型中间件,koa的中间件就不是那么直观了。先看一张图
把洋葱的一圈看做是一个中间件,直线型就是从第一个中间件走到最后一个,但是洋葱型就很特殊了,最早use的中间件在洋葱的最外层,开始的时候会按顺序走到所有中间件,然后按照倒序再走一遍所有的中间件,相当于每个中间件都会进入两次。这就给了我们更多的操作空间。
看下面一段代码
const koa = require('koa');
let server = new koa();
server.use(async (ctx, next) => {
console.log('a-1');
next();
console.log('a-2');
})
server.use(async (ctx, next) => {
console.log('b-1');
next();
console.log('b-2');
})
server.use(async (ctx, next) => {
console.log('c');
})
server.listen(3000);
代码执行后命令行输出顺序为
- koa官方文档上把外层的中间件称为"上游",内层的中间件为"下游"。
一般的中间件都会执行两次,调用next之前为第一次,调用next时把控制按顺序传递给下游的中间件。当下游不再有中间件或者中间件没有执行next函数时,就将依次恢复上游中间件的行为,让上游中间件执行next之后的代码。
- 如果某一个中间件中有异步代码,最好使用async/await处理异步。
使用方式我们已经搞清了,下面就看看内部实现原理
源码解析
在github上找到koa的源码,核心的函数都在application.js中。
经过上一篇connect源码的学习,这部分源码读起来应该也是压力不大。全部代码也就200多行,这里我只截取其中比较重要的部分。
'use strict';
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);
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
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;
}
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;
}
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);
}
};
1.先来重点看下use函数
第一个if是用来做校验的,第二个if是用来兼容generator函数的。
use的最后执行了this.middleware.push(fn);
就是将中间件放到middleware数组中,类似connect中的stack数组。
2.再来看下callback函数callback
函数的返回值就是listen
函数中执行createServer
函数的回调,也就是handleRequest
函数,也可以理解成是用来响应request
事件的函数。
3.注意在callback中执行this.handleRequest(ctx, fn);
的时候这个fn是中间件数组通过compose函数处理后返回的值。那在看handleRequest
函数中最后就调用了这个函数,那么在这之前就要知道compose函数对中间件数组做了什么?
找到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) {
// 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
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)
}
}
}
}
前面两段很明显是在验证数组和函数的类型。然后就是返回一个函数,这个函数就是callback函数中的fn,也是在传入handleRequest
函数中最后调用的那个函数。
先总结下上面的内容:
- 在use函数中先把中间件添加到
this.middleware
数组中。 - callback函数中先通过compose函数对
this.middleware
数组进行操作,将数组转换为一个函数赋值给fn(注意这个函数比较特殊,下面会详细讲解),然后再通过handleRequest
函数去调用这个fn函数,显而易见这个函数一定返回Promise对象。
这个流程如果搞清楚后,再来看看这个compose到底做了什么。
实际上compose就是返回一个函数,函数内利用闭包实现了对中间件数组的遍历,返回一个Promise对象,并且resolve回调就是执行当前索引对应的中间件函数,并且将参数next赋值为内部闭包函数的调用,调用的同时将索引值+1,这样在中间件中执行next()就相当于执行了dispatch(i+1),自然就找到了下一个中间件。如果中间件没有执行next或者遍历了数组中全部的中间件后,自然就开始一级一级向上执行每个中间件next后的代码。这就是洋葱圈的实现。
这里的逻辑稍微有点饶,如果还是不懂也没关系,我将代码流程简化,通过图片再来说下整个实现过程。
整个核心还是compose函数。如果将这个函数简化到极致,就是如下的代码
function a(){
console.log('a-1');
next(b);
console.log('a-2');
}
function b(){
console.log('b-1');
next(c);
console.log('b-2');
}
function c(){
console.log('c');
}
function next(fn){
return Promise.resolve(fn());
}
// 输出结果为
// a-1
// b-1
// c
// b-2
// a-2
总结
到此我们已经分析了express中间件和koa中间件的实现原理。
二者的源码都是非常简洁,并且设计巧妙、可读性高。很多细节处理都值得借鉴。
希望自己在源码阅读的道路上越走越远。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。