最近经常使用koa进行服务端开发,迷恋上了koa的洋葱模型,觉得这玩意太好用了。而且koa是以精简为主,没有很多集成东西,所有的东西都需按需加载,这个更是太合我胃口了哈哈哈哈。相对与express的中间件,express的中间件使用的是串联,就像冰糖葫芦一样一个接着一个,而koa使用的V型结构(洋葱模型),这将给我们的中间件提供更加灵活的处理方式。基于对洋葱模型的热衷,所以对koa的洋葱模型进行一探究竟,不管是koa1还是koa2的中间件都是基于koa-compose进行编写的,这种V型结构的实现就来源于koa-compose。
附上源码先:
function compose (middleware) {
// 参数middleware 是一个中间件数组,存放我们用app.use()一个个串联起来的中间件
// 判断中间件列表是否为数组,如果不为数组,则抛出类型错误
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!')
}

/**
1. @param {Object} context
2. @return {Promise}
3. @api public

*/

return function (context, next) {

// 这里next指的是洋葱模型的中心函数

// context是一个配置对象,保存着一些配置,当然也可以利用context将一些参数往下一个中间传递



// last called middleware #

let index = -1 // index是记录执行的中间件的索引

return dispatch(0) // 执行第一个中间件 然后通过第一个中间件递归调用下一个中间件



function dispatch (i) {

// 这里是保证同个中间件中一个next()不被调用多次调用

// 当next()函数被调用两次的时候,i会小于index,然后抛出错误

if (i <= index) return Promise.reject(new Error('next() called multiple times'))

index = i

let fn = middleware[i] // 取出要执行的中间件

if (i === middleware.length) fn = next // 如果i 等于 中间件的长度,即到了洋葱模型的中心(最后一个中间件)

if (!fn) return Promise.resolve() // 如果中间件为空,即直接resolve

try {

// 递归执行下一个中间件 (下面会重点分析这个)

return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

} catch (err) {

return Promise.reject(err)

}

}
}
}


function compose (middleware) {
// 参数middleware 是一个中间件数组,存放我们用app.use()一个个串联起来的中间件
// 判断中间件列表是否为数组,如果不为数组,则抛出类型错误
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!')
}

/**
1. @param {Object} context
2. @return {Promise}
3. @api public

*/

return function (context, next) {

// 这里next指的是洋葱模型的中心函数

// context是一个配置对象,保存着一些配置,当然也可以利用context将一些参数往下一个中间传递



// last called middleware #

let index = -1 // index是记录执行的中间件的索引

return dispatch(0) // 执行第一个中间件 然后通过第一个中间件递归调用下一个中间件



function dispatch (i) {

// 这里是保证同个中间件中一个next()不被调用多次调用

// 当next()函数被调用两次的时候,i会小于index,然后抛出错误

if (i <= index) return Promise.reject(new Error('next() called multiple times'))

index = i

let fn = middleware[i] // 取出要执行的中间件

if (i === middleware.length) fn = next // 如果i 等于 中间件的长度,即到了洋葱模型的中心(最后一个中间件)

if (!fn) return Promise.resolve() // 如果中间件为空,即直接resolve

try {

// 递归执行下一个中间件 (下面会重点分析这个)

return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

} catch (err) {

return Promise.reject(err)

}

}
}
}

看到这里,如果下面的那些能够理解,那么下面的可以不用看的,还是不能理解的就继续往下看,详细一点的分析。首先,我们用app.use()添加一个中间件,在koa的源码里app.use()这个方法就是将一个中间件push进middleware这个中间件列表里。源码里是这么写的(这个比较简单 不做分析):

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(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;
}
compose这个方法传入一个中间件列表middleware,这个列表就是我们使用use()添加进去的方法列表,首先会判断列表是否为数组,中间件是否为方法,如果不是就直接抛出类型错误。

compose返回的是一个函数,这里使用闭包来缓存中间件列表,然后这个函数接收两个参数,第一个参数是context,是一个存放配置信息的对象。第二份参数是一个next方法,也是洋葱模型的中心或者说是V型模型的拐点。

创建一个index变量来保存执行的中间件索引,然后从第一个中间件开始开始递归执行。
compose返回的是一个函数,这里使用闭包来缓存中间件列表,然后这个函数接收两个参数,第一个参数是context,是一个存放配置信息的对象。第二份参数是一个next方法,也是洋葱模型的中心或者说是V型模型的拐点。创建一个index变量来保存执行的中间件索引,然后从第一个中间件开始开始递归执行。
let index = -1
return dispatch(0)
let index = -1
return dispatch(0)dispatch方法就是执行中间件,先判断索引,如果i小于index那么说明在同一个中间件里执行了两次或两次以上的next函数,如果i>index则说明该中间件还未执行,那么将该中间件的所以记录下来

if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i取出该中间件,如果i等于中间件的长图,则说明执行到了洋葱模型的中心,则最后一个中间件,如果中间件为空,那么就直接resovle掉


let fn = middleware[i]

if(i === middleware.length){

fn = next

}

if(!fn){

return Promise.resolve()

}

let fn = middleware[i]

if(i === middleware.length){

fn = next

}

if(!fn){

return Promise.resolve()

}到了最刺激的部分了,也是有点绕的部分了,首先为啥return的是一个Promise的对象(Promise.resolve也是一个promise对象)呢,因为我们await next()的时候,await是等待且执行一个async函数的完成,async会默认返回一个promise对象,所以这里return的是一个promise对象。我们在每个中间里面await mext() next()指的就是下一个中间件,也就是

fn(context, function next () {

return dispatch(i + 1)

})

fn(context, function next () {

return dispatch(i + 1)

})
所以我们上一个中的await 等待的就是dispatch(i+1)的执行完成,dispatch返回的是Promise.resolve(fn(context, function next () { xxxx })),这样来看虽然一开始只执行了dispatch(0),但是是由这个函数形成了一条执行链。
以三个中间件执行为例,dispatch(0)执行后就形成:
Promise.resolve( // 第一个中间件
function(context,next){ // 这里的next第二个中间件也就是dispatch(1)

// await next上的代码 (中间件1)

await Promise.resolve( // 第二个中间件

function(context,next){ // 这里的next第二个中间件也就是dispatch(2)

// await next上的代码 (中间件2)

await Promise.resolve( // 第三个中间件

function(context,next){ // 这里的next第二个中间件也就是dispatch(3)

// await next上的代码 (中间件3)

await Promise.resolve()

// await next下的代码 (中间件3)

}

)

// await next下的代码 (中间件2)

}

)

// await next下的代码 (中间件2)
}
)

Promise.resolve( // 第一个中间件
function(context,next){ // 这里的next第二个中间件也就是dispatch(1)

// await next上的代码 (中间件1)

await Promise.resolve( // 第二个中间件

function(context,next){ // 这里的next第二个中间件也就是dispatch(2)

// await next上的代码 (中间件2)

await Promise.resolve( // 第三个中间件

function(context,next){ // 这里的next第二个中间件也就是dispatch(3)

// await next上的代码 (中间件3)

await Promise.resolve()

// await next下的代码 (中间件3)

}

)

// await next下的代码 (中间件2)

}

)

// await next下的代码 (中间件2)
}
)
先执行await上面的代码,然后等待最后一个中间件resolve一个个往上传递,这就形成了一个洋葱模型。
最后附上测试代码:
async function test1(ctx, next) {

console.log('中间件1上');

await next();

console.log('中间件1下');
};

async function test2(ctx, next) {

console.log('中间件2上');

await next();

console.log('中间件2下');
};

async function test3(ctx, next) {

console.log('中间件3上');

await next();

console.log('中间件3下');
};
let middleware = [test1, test2, test3];

let cp = compose(middleware);

cp('ctx', function() {

console.log('中心');
});
async function test1(ctx, next) {

console.log('中间件1上');

await next();

console.log('中间件1下');
};

async function test2(ctx, next) {

console.log('中间件2上');

await next();

console.log('中间件2下');
};

async function test3(ctx, next) {

console.log('中间件3上');

await next();

console.log('中间件3下');
};
let middleware = [test1, test2, test3];

let cp = compose(middleware);

cp('ctx', function() {

console.log('中心');
});OK,到这里koa2的中间件核心(koa-compose)就解析完成了,一开始看的时候,也被绕了好久,多看几遍多分析一步一步捋顺。koa1的中间件等过几天有时间再补上吧,koa1是基于generator,源码比起koa2相对简单。最近在看koa2源码,等有时间再继续更新koa一些源码的分析。