讲讲koa-compose
的实现思路
阅读前所需知识
- 拥有Node.js语言基础
- 了解http模块
- 有Koa框架使用经验
compose的使用
async function middle1(ctx, next) {
console.log(`middle1 pt 1`);
const res = await next();
console.log(`middle1 pt 2`);
return res;
}
async function middle2(ctx, next) {
console.log(`middle2 pt 1`);
const res = await next();
console.log(`middle2 pt 2`);
return 'foo';
}
const middleA = compose([middle1, middle2]);
middleA().then((res) => {
console.log('output!!!========================');
console.log(res);
}).catch((err) => {
console.error('catched!!!========================');
console.error(err);
});
// 执行结果
// middle1 pt 1
// middle2 pt 1
// middle2 pt 2
// middle1 pt 2
// output!!!========================
// foo
compose实现
要理解compose实现的最好办法就是尝试自己写一个,整理一下实现要点:
-
compose(middles)
返回的必须也是个中间件(我们称为聚合中间件),即return
一个function (ctx, next): Promise<any> => {}
签名函数 -
聚合中间件的实现逻辑首先想到的是对
middles
做一次遍历,依次把下一个中间件函数放入上一个中间件的next
参数中
第一版
function compose(middles) {
let curIdx = 0;
return async (ctx, next) => {
middles[curIdx](ctx,
middles[curIdx + 1].bind(null, ctx,
middles[curIdx + 2].bind(null, ...)));
};
}
写到...处写不下去了,意识到自己是在硬编码一个递归流程。第二版尝试用递归函数实现
第二版
function compose(middles) {
let curIdx = 0;
return (ctx, next) => {
// 根据整理的实现要点1,recursive(i)函数必须return一个Promise
return recursive(curIdx);
function recursive(i) { // 执行index为i的中间件
if (i >= middles.length) {
// 递归到越界时return一个空Promise
// 即最后一个中间件调用next()时返回一个空Promise
return Promise.resolve();
}
const curMiddle = middles[i]; // 找到当前中间件
// 执行当前中间件,next传入高阶函数。做法是对recursive函数进行参数绑定,参数绑定为下一顺位的索引。
// 这样一来当前中间件内部在执行next()时就相当于执行recursive(i+1)
return curMiddle(ctx, recursive.bind(null, i+1));
}
};
}
// 执行了一下居然可以了!!!
// middle1 pt 1
// middle2 pt 1
// middle2 pt 2
// middle1 pt 2
// output!!!========================
// foo
compose()
组合成的聚合中间件函数function (ctx, next): Promise<any> => {}
,next
参数实际是无效的。执行完所有中间件后并不能向后传递。导致的具体问题是无法执行多层聚合:
// **聚合中间件A**和**聚合中间件B**,聚合为一个**聚合中间件C**。
const middleA = compose([middle1, middle2]);
const middleB = compose([middle3, middle4]);
const middleC = compose([middleA, middleB]);
// 执行middleC仅会执行middleA的中间件,因为middleA的next()并不能指向middleB
middleC().then((res) => {
console.log('output!!!========================');
console.log(res);
}).catch((err) => {
console.error('catched!!!========================');
console.error(err);
});
我们可以判断当越界时,即执行到最后一个middleware后,执行next
第三版
function compose(middles) {
let curIdx = 0;
return (ctx, next) => {
return recursive(curIdx);
function recursive(i) {
if (i >= middles.length) {
return Promise.resolve();
}
if (i < middles.length) {
const curMiddle = middles[i];
return curMiddle(ctx, recursive.bind(null, i+1));
} else if (next) { // 越界后判断next是否存在,存在就继续传递执行下去
return next(ctx, () => Promise.resolve());
} else {
return Promise.resolve();
}
}
};
}
recursive(i)
函数返回值并不能保证return
一个Promise
,当curMiddle
返回非Promise
对象时,compose()
组合成的聚合中间件将不再满足上述整理的实现要点1。导致application.js执行中间件报错:
application.js
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
// fnMiddleware为compose()组合成的 聚合中间件
// 执行报错!!! TypeError: fnMiddleware(...).then is not a function
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
解决思路也很简单,包一层Promise.resolve即可
第四版
function compose(middles) {
let curIdx = 0;
return (ctx, next) => {
return recursive(curIdx);
function recursive(i) {
if (i < middles.length) {
try {
const curMiddle = middles[i];
// 无论curMiddle是否返回Promise,都返回Promise
return Promise.resolve(curMiddle(ctx, recursive.bind(null, i+1)));
} catch (e) { // 如果curMiddle同步执行过程中抛出异常,也需要返回一个reject状态的Promise
return Promise.reject(e);
}
} else if (next) { // 越界后判断next是否存在,存在就继续传递执行下去
// 无论next是否返回Promise,都返回Promise
return Promise.resolve(next(ctx, () => Promise.resolve()));
} else {
return Promise.resolve();
}
}
};
}
当某个中间件执行两次next()时递归会分叉,后面的所有中间件都会执行两次。类似于平行宇宙分叉出不同时间线那样,这个是我们不希望看到的,我们希望只存在一个洋葱!
解决思路也很简单,因为聚合中间件是顺序执行middleware队列,因此每次执行记一下当前的中间件队列索引,如果中间件执行两遍next(),马上就能检测出来:
第五版
function compose(middles) {
return (ctx, next) => {
let lastExec = -1; // 记录上一次正在执行的中间件队列索引
return recursive(0);
function recursive(i) {
if (i < middles.length) {
// 如果当前执行的索引和上一次执行的索引相同,那么一定是中间件重复执行next()
if (lastExec === i) {
throw new Error('next() exec multiple times'); // 应该立刻停止这种情况的发生
}
lastExec = i;
try {
const curMiddle = middles[i];
// 无论curMiddle是否返回Promise,都返回Promise
return Promise.resolve(curMiddle(ctx, recursive.bind(null, i+1)));
} catch (e) { // 如果curMiddle同步执行过程中抛出异常,也需要返回一个reject状态的Promise
return Promise.reject(e);
}
} else if (next) { // 越界后判断next是否存在,存在就继续传递执行下去
// 无论next是否返回Promise,都返回Promise
return Promise.resolve(next(ctx, () => Promise.resolve()));
} else {
return Promise.resolve();
}
}
};
}
至此已经完成了compose的实现,我们来看看效果:
function middle1(ctx, next) {
console.log(`middle1 pt 1`);
const res = next();
console.log(`middle1 pt 2`);
return res;
}
async function middle2(ctx, next) {
console.log(`middle2 pt 1`);
const res = await next();
console.log(`middle2 pt 2`);
return res;
}
async function middle3(ctx, next) {
console.log(`middle3 pt 1`);
const res = await next();
console.log(`middle3 pt 2`);
return res;
}
async function middle4(ctx, next) {
console.log(`middle4 pt 1`);
const res = await next();
console.log(`middle4 pt 2`);
return 'foo';
}
const middleA = compose([middle1, middle2]);
const middleB = compose([middle3, middle4]);
const middleC = compose([middleA, middleB]);
middleC().then((res) => {
console.log('output!!!========================');
console.log(res);
}).catch((err) => {
console.error('catched!!!========================');
console.error(err);
});
// 执行结果
// middle1 pt 1
// middle2 pt 1
// middle3 pt 1
// middle4 pt 1
// middle1 pt 2
// middle4 pt 2
// middle3 pt 2
// middle2 pt 2
// output!!!========================
// foo
koa-compose源码
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
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
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)
}
}
}
}
源码由于没有注释,if-else流程也做了精简,因此理解上还是有点费劲的。在实现原理上和我们自己写的是完全一致的。