中间件的作用
大部分人用koa应该是用来实现后端服务的。后端服务最常见的就是实现接口了。但这些接口一般有一些相同的功能。例如日志打印请求时间,请求参数等。每写一个接口都写这些功能,不仅浪费时间,代码也难看。因此将这些通用功能提取成中间件,请求来了之后,经过各个中间件进行处理。
存储中间件
首先中间件是由我们,koa的用户定义的。所以koa要将我们定义的中间件存储起来。我们应该都写过中间件,知道每个中间件其实就是一个方法。然后中间件的执行是有顺序的。所以要存储一些有顺序的方法,最简单的就是使用一个数组。添加一个中间件就是为数组push一个方法。
我们使用过koa,都知道koa添加中间件的方式是app.use(fn)。app就是koa模块的实例。
class Koa {
constructor(){
this.middlewares = []
}
use(fn){
this.middlewares.push(fn)
}
}
const app = new Koa()
app.use((ctx, next) => {})
console.log(app.middlewares.length)
console.log(app.middlewares[0].toString())
稍作思考,我们就知道让数组middlewares作为app的一个属性,为app提供一个方法use,功能就是为middlewares数组push一个方法。上面的代码会在控制台上打印出1后打印出(ctx,next)=>{},这里就不贴图了。
中间件执行顺序
现在我们已经能够存储中间件了,那么如何让他们按照一定的顺序去执行呢?最简单的就是链式执行了。就是一个执行完之后去执行另一个。代码也很简单。
function sleep(time) {
return new Promise((resolve)=> {
setTimeout(()=>{
resolve()
}, time)
})
}
let middleWare0 = async function (ctx, next) {
let startTime = Date.now()
if(next){
await next()
}
console.log(Date.now() - startTime)
}
let middleWare1 = async function (ctx, next) {
if(next){
await next()
}
await sleep(1000)
}
async function compose(middlewares, ctx) {
for(let middleware of middlewares){
await middleware(ctx)
}
}
compose([middleWare0, middleWare1], {})
compose方法就是用来链式执行中间件的。sleep函数就是用来模拟异步操作的。一共有两个中间件。确实是先执行了中间件0,然后执行了中间件1。这时应该能看出上述代码有些问题:
首先,我们在平常写代码的时候是没有if(next)这个判断的。这里去掉这个判断就会报错。因为在执行中间件的时候根本没有给他next这个参数。next是undefined,而不是一个函数,所以会报错。
其次,middleWare0的代码功能其实是想等待后面的中间件执行完之后再console.log,但现在确是立刻就输出了。时间差也是0毫秒左右。而我们要等待middleWare1完成,应该是有1000ms左右。
**koa要实现的中间件不是简单的链式执行。而是前面的中间件能够控制后面的中间件该何时执行。**很高端吧。其实,不用害怕。中间件只不过是一个函数罢了,而他想要控制另一个函数该如何执行,最简单就是把这个函数告诉他,也就是作为一个参数传递进去。我们把compose改一改 。
let compose = async function (ctx) {
await middlewares[0](ctx,middlewares[1])
}
首先我们考虑只有两个中间件的情况。上述代码执行了中间件0,并将ctx和中间件1传给了它。也就是在中间件0中的next就是中间件1。这和我们熟悉的不一样,next在执行时并不需要我们传参数。这时我们可以推断出next这个函数应该是这种形式:
next[i] = async function(){
await middlewares[i](ctx,next[i+1])
}
next[i]应该返回的是一个异步函数,里面执行了当前第i个中间件,并将ctx,以及next[i+1]传递给第i个中间件。
不难发现,这是一个递归的结构。还有对比上面两段代码。其实结构是一样的。所以最终能够写出这段代码:
function compose(ctx, i) {
return async function () {
if(middlewares[i]){
await middlewares[i](ctx, compose(ctx, i+1))
}
}
}
执行compose(ctx,i)便能得到一个异步函数fn。fn的第一步就是判断中间件0是否存在,如果存在就调用中间件0,第一个参数为ctx,第二个参数为compose(ctx,i+1)这个函数的执行结果,也就是下一个中间件对应的异步函数。下面举例看下fn。
let fn = compose(ctx, 0)
fn = async function () {
await middlewares[0](ctx, async function () {
await middlewares[1](ctx, async function () {
await middlewares[2](ctx, async function () {
...
})
})
})
}
fn的格式就是上面那样(少了判断中间件是否存在)。
通过这种方式来执行中间件,每个中间件都能控制什么时候来执行下一个中间件。
不过我们还是少了一步,就是如何把middlewares这个数组传递进来。我的实现方式不是很好,但看起来比较简单。
let middlewares = []
let setMiddleWare = function (middlewaresArg) {
middlewares = middlewaresArg
}
function compose(ctx, i) {
return async function () {
if(middlewares[i]){
await middlewares[i](ctx, compose(ctx, i+1))
}
}
}
exports.setMiddleWare = setMiddleWare
exports.compose = compose
测试代码如下:
function sleep(time) {
return new Promise((resolve)=> {
setTimeout(()=>{
resolve()
}, time)
})
}
let middleWare0 = async function (ctx, next) {
let startTime = Date.now()
await next()
console.log(Date.now() - startTime)
}
let middleWare1 = async function (ctx, next) {
next()
await sleep(1000)
console.log('中间件1')
}
let middleWare2 = async function (ctx, next) {
await sleep(2000)
await next()
console.log('中间件2')
}
const compose = require('./compose')
compose.setMiddleWare([middleWare0, middleWare1, middleWare2])
let fn = compose.compose({},0)
fn()
.catch(err => {
console.log(err)
})
compose便是上面的模块。若代码没有问题,则应该先执行中间件0的await next()前面的代码,然后执行中间件1,中间件1第一句就是next()也就是异步执行中间件2,这时进入中间件2,中间件2第一句会执行等待定时器2秒。这时回到中间件1,中间1会等待定时器1秒,约一秒后中间件1定时完成,输出“中间件1”。中间件0等待中间件1执行完成,输出时间差,约为1000。约1秒后,中间件2定时完成,输出“中间件2”。
下面是代码的输出结果。
具体的等待时间可以自己运行验证下。
到现在为止我们基本完成了中间件的所有实现代码。那么让我们看下koa是如何实现的吧。
koa的中间件实现
//以下代码均去掉了部分无关代码
class Application extends Emitter {
constructor() {
super();
this.middleware = [];
}
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
callback() {
const fn = compose(this.middleware);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
}
这时看use和callback方法是不是有一些豁然开朗了?
koa的use和我们写的相比,主要就是多了一个对入参fn的校验。 compose中,middleware数组的传入方式不同,他是直接传进了compose。让我们看下koa的compose是如何保存middleware的。
function compose (middleware) {
//去掉了校验代码
return function (context, next) {
let index = -1
return dispatch(0)
function dispatch (i) {
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
koa保存middleware其实用了闭包。compose(middleware)返回了一个function,而这个function用到了middleware。所以只要外面引用这个function,middleware就会一直在这个作用域存在。
compose执行后,返回的function支持两个参数,一个是context,一个是next。next为middleware数组后面的中间件。看下代码中的fn,fn随着i的增加,而取值为middleware[i],当i为middleware的长度时,也就是没有这个中间件时,fn取值为传入的next,所以传入的next可以理解为next[middleware.length] 。
接着看,确定好了fn后,执行了fn,传入了参数context,以及next[i+1],和我们刚刚的代码是一样的。不过由于他用的是promise,所以外面包裹了一层Promise.resolve。方便后面的promise链调用。而我们是用的async,功能都是差不多的。