Koa is a new web framework designed by the team behind Express, which aims to be a smaller, more expressive, and more robust foundation for web applications and APIs. By leveraging async functions, Koa allows you to ditch callbacks and greatly increase error-handling. Koa does not bundle any middleware within its core, and it provides an elegant suite of methods that make writing servers fast and enjoyable.
上面是koa的官网的简单介绍,只需要关心一点: 中间件机制是koa的核心。 可以说,理解了中间件也就理解了koa框架的精华。而实现中间件机制的关键是compose函数。
洋葱模型的基本介绍
每个中间件需要依次处理request和response请求。这种中间件模型称为洋葱模型(Onion model)
上面的代码可以记录response请求的时间。可以看到,利用koa实现logger
,代码相当简洁。
compose 1.0 版本实现
五年前,前端没有async的情况下,compose
的实现其实相当复杂,利用了Thunk、generator、Co
来进行异步管理。不过,可以看到即使前端变化非常之大,compose
的核心理念依然没有发生改变。
不考虑任何异步情况,实现洋葱模型
function fn1(next) {
console.log(1);
next();
}
function fn2(next) {
console.log(2);
next();
}
function fn3(next) {
console.log(3);
next();
}
middleware = [fn1, fn2, fn3]
function compose(middleware){
function dispatch (index){
if(index == middleware.length) return ;
var curr;
curr = middleware[index];
// 这里使用箭头函数,让函数延迟执行
return curr(() => dispatch(++index))
}
dispatch(0)
};
compose(middleware);
复制代码
根据分析,最后实际上将几个函数通过串联的方式进行了连接:
对于 fn1
来说,next函数就是 ()=> fn2( () => fn3())
考虑无promise的异步情况。(callback+generator)
当出现generator类型的时候,我们next允许接受Generator类型
function * fn1(next) {
console.log(1);
//如果没有yield,就无法进行递归调用
yield next();
}
function * fn2(next) {
console.log(2);
yield next();
}
function * fn3(next) {
console.log(3);
yield next();
}
middleware = [fn1, fn2, fn3]
function compose(middleware){
function dispatch (index){
if(index == middleware.length) return ;
var curr;
function* prev(){
console.log('none');
}
curr = middleware[index]
console.log(curr);
return curr(() => dispatch(++index))
}
return dispatch(0)
};
compose(middleware)
复制代码
这时候运行,compose(middleware)
实际上是一个 [GeneratorFunction fn1]
的类型。
如果我们需要达到第一种代码的运行效果,手动执行如下:
k0 = compose(middleware).next()
k1 = k0.value
k2 = k1.next().value
k3 = k2.next()
//输出为1 2 3
复制代码
中间件多的话,手动执行就无法实现。可以增加一个自动执行generator的函数:
function co (gen) {
let g = gen;
function next(nex) {
let result = nex.next();
if(result.done) return result.value;
if(typeof result.value == 'object') {
next(result.value);
}
}
next(g);
}
//再次执行, 输出为123
co(compose(middleware))
复制代码
generator+co的方式实现中间件代码逻辑相当复杂,上面只是考虑了三种情况下的一种。
compose 2.0 版本实现
利用promise实现
function compose(middleware){
function dispatch (index){
if(index == middleware.length) return ;
var curr;
function prev(){
next;
}
curr = middleware[index];
// 这里使用箭头函数,让函数延迟执行
return curr(() => dispatch(++index))
}
dispatch(0)
};
复制代码
当异步操作使用 async/await
的时候,上面compose
的实现已经可以解决异步问题。(async
函数可以看作同步函数)。但是,异步操作代码,如果抛出错误,上面的代码无法对错误进行捕捉。
function * fn1(next) {
console.log(1);
throw new Error('错误无法捕捉');
//如果没有yield,就无法进行递归调用
yield next();
}
复制代码
考虑到,async其实返回一个Promise类型,我们将所有的中间件函数包裹成一个Promise对象。然后,通过 reject
和 catch
来进行错误处理。
function compose(middleware){
function dispatch (index){
if(index == middleware.length) return Promise.resolve();
var curr;
function prev(){
next;
}
curr = middleware[index];
// 修改成Promise对象
return Promise.resolve(curr(() => dispatch(++index)));
}
dispatch(0)
};
复制代码
函数式风格实现
从上面的实现我们可以看出来,以上所有的实现,都无非是把中间件函数 fn1
, fn2
, fn3
包裹成下列形式:
那么,对于习惯使用函数式编程的人来说,这其实是一个右向reduce的过程。
function compose () {
return this.middlewares.reduceRight( (a, b) => () => b(a), () => {})();
}
复制代码
然后,如果需要修改返回类型是Promise类型,那么可以简单的修改为:
function compose () {
return this.middlewares.reduceRight( (a, b) => () => Promise.resolve(b(a)), () => {})();
}
复制代码