中间件特性
先写一段贯穿全文的 koa 的代码
const Koa = require('koa');
let app = new Koa();
const middleware1 = async (ctx, next) => {console.log(1);
await next();
console.log(6);
}
const middleware2 = async (ctx, next) => {console.log(2);
await next();
console.log(5);
}
const middleware3 = async (ctx, next) => {console.log(3);
await next();
console.log(4);
}
app.use(middleware1);
app.use(middleware2);
app.use(middleware3);
app.use(async(ctx, next) => {ctx.body = 'hello world'})
app.listen(3001)
// 输出 1,2,3,4,5,6
await next()
使每个 middleware 分成,前置操作,等待其他中间件操作可以观察到中间件的特性有:
- 上下文 ctx
- await next()控制前后置操作
- 后置操作类似于数据解构 - 栈,先进后出
promise 的模拟实现
Promise.resolve(middleware1(context, async() => {return Promise.resolve(middleware2(context, async() => {return Promise.resolve(middleware3(context, async() => {return Promise.resolve();
}));
}));
}))
.then(() => {console.log('end');
});
从这段模拟代码我们可以知道 next()返回的是 promise,需要使用 await 去等待 promise 的 resolve 值。promise 的嵌套就像是洋葱模型的形状就是一层包裹着一层,直到 await 到最里面一层的 promise 的 resolve 值返回。
思考:
- 如果 next()不加 await 执行顺序是什么呢?
在这个例子里面如果只是next()
执行顺序跟await next()
是一样的,因为 next 的前置操作是同步的 -
如果前置操作是异步的操作呢?
const p = function(args) { return new Promise(resolve => {setTimeout(() => {console.log(args); resolve();}, 100); }); }; const middleware1 = async (ctx, next) => {await p(1); // await next(); next(); console.log(6); }; const middleware2 = async (ctx, next) => {await p(2); // await next(); next(); console.log(5); }; const middleware3 = async (ctx, next) => {await p(3); // await next(); next(); console.log(4); }; // 输出结果:1,6,2,5,3,4
当程序执行到 middleware1,执行到
await p(1)
等待 promise 值返回跳出然后到下一个事件循环时,执行 next()也就是执行到 middleware2,再执行到await p(2)
等待 promise 值返回跳出 middleware2,回到 middleware1 继续执行 console.log(6), 以此类推输出顺序为 1.6.2.5.3.4
Promise 的嵌套虽然可以实现中间件流程,但是嵌套的代码会产生可维护性和可读性的问题,也带来中间件扩展的问题。
Koa.js 中间件引擎是有 koa-compose 模块来实现的,也就是 Koa.js 实现洋葱模型的核心引擎。
koa-compose 实现
this.middleware = [];
use(fn) {this.middleware.push(fn);
……
}
callback() {const fn = compose(this.middleware);
……
}
function compose (middleware) {return function (context, next) {
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)
}
}
}
}
Koa 实现的代码非常简洁,我们在使用 use 的时候将 middleware 存在一个数组里面,当拦截到请求时执行 callback 方法,callback 中调用了 compose,compose 方法使用递归执行中间件,遍历完成返回promise.resolve()
,实际最后执行的代码也是上面所讲的 promise 嵌套的形式。
扩展:深入理解 babel 编译后的 await
通常我们的都会说 await 阻塞后面的操作等待 promise 的 resolve 返回值或者其他值,如果没有 await 这个语法糖,要怎么去实现呢?babel 是怎么编译的呢
如果我们直接把代码片段一的三个中间件编译,你会发现多了 regeneratorRuntime
和_asyncToGenerator
两个函数。为了理解 regeneratorRuntime
我们要先理解Generator
。
Generator
Generator 实际上是一个特殊的迭代器
let gen = null;
function* genDemo(){console.log(1)
yield setTimeout(()=>{console.log(3);
gen.next();// c.},100)
console.log(4)
}
gen = genDemo();// a. 调用 generator,该函数不执行,也就是还没有输出 1,返回的是指向内部状态的遍历对象。gen.next(); // b. generator 函数开始执行,输出 1,遇到第一个 yeild 表达式停下来, 调用 gen.next()返回一个对象{value: 10, done:false},这里的 value 表示 setTimeout 的一个标识值,也就是调用 clearTimeout 的参数,是一个数字。done 表示遍历还没有结束。