中间件概念
在NodeJS中,中间件主要是指封装所有Http请求细节处理的方法。一次Http请求通常包含很多工作,如记录日志、ip过滤、查询字符串、请求体解析、Cookie处理、权限验证、参数验证、异常处理等,但对于Web应用而言,并不希望接触到这么多细节性的处理,因此引入中间件来简化和隔离这些基础设施与业务逻辑之间的细节,让开发者能够关注在业务的开发上,以达到提升开发效率的目的。
中间件的行为与Vue里面的过滤器非常相似,就是在进入具体的业务处理之前,先让过滤器处理。中间件的最常见的处理模型叫做洋葱模型:
Koa实现原理
Koa.js中间件机制是由koa-compose模块来实现的,也就是Koa.js实现洋葱模型的核心模块。Koa的中间件机制主要有以下两个重要的部分:
1、context的保存和传递
context是一个上下文对象,Koa针对每次请求都会创建一个上下文对象,这个对象会在中间件之间传递。
2、中间件的管理和next的实现
正如前文提到的,中间件一般不止一个,那么就需要考虑以下三个问题:
1、如何保存多个中间件
2、中间件的存放顺序
3、如何自动触发下个中间件处理函数,也就是如何实现next方法
带着以上问题,我们来看一下相关源码:
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
// const compose = require('koa-compose');
callback() {
// this.middleware是保存所有中间件处理函数的数组,中间件处理函数是通过 koa实例.use()方法添加到this.middleware里面的
// fn是koa-compose模块处理后的结构
// koa-compose会将所有中间件函数构成一个调用链
const fn = compose(this.middleware);
......
const handleRequest = (req, res) => {
// Koa特有的封装上下文对象方法,也就是上文提到的context对象
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest; // handleRequest是请求的处理函数, 最后会作为http.createServer()的参数
}
// fnMiddleware对应前文const fn = compose(this.middleware)里面的fn(中间件调用链)
handleRequest(ctx, fnMiddleware) {
......
// handleResponse是真正处理业务的函数
// onerror是错误处理函数
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
以上是Koa里面的主体逻辑,接下来我们把关注点放到koa-compose模块的实现,我们先来看看koa-compose模块的源代码:
源码代码量很少,理解起来却并不是那么容易。我们一步一步来,首先compose函数返回的也是一个函数,返回的函数就是前文的fnMiddleware函数,fnMiddleware可以接收两个参数:context(针对某一次请求的上下文对象),next(允许用户在中间件队尾加上指定的处理函数,也可以认为是一个中间件函数,Koa里面并没有使用这个参数)。
compose函数使用了js闭包:内部定义了一个index变量,以及引用它的dispatch函数;设置这个闭包的意图是防止一个请求多次调用同一个中间件。
接下来我们仔细说说dispatch函数,dispatch函数内部有这么两行:
if (i === middleware.length) fn = next // next是用户可自定义的加在中间件队尾的处理函数
if (!fn) return Promise.resolve()
以上两行代码的意思是当用户并没有传入next参数时,默认返回一个resolve状态的promise对象。到这里为止,我们可以猜想compose函数期待返回的是一个promise对象,或者说compose函数的目标就是返回一个promise对象。那到底是不是呢?我们继续往下看:
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
Promise.resolve和Promise.reject函数返回的都是promise对象,只是状态不同。至此为止,我们已经证明了我们的猜想,compose函数的返回结果是promise对象。
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
这一行代码的应该是整个compose方法的核心,也是创建中间件队列的关键!
首先简单了解一下Promise.resolve方法:
1、允许调用时不带参数,直接返回一个resolved状态的 Promise 对象;
2、如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例;
3、参数是一个thenable对象,thenable对象指的是具有then方法的对象;
4、如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved。这种情况比较特殊,例子如下:
const p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
});
// Hello
Promise.reject方法的用法跟Promise.resolve方法基本一致,只是返回promise对象的状态不一样而已。
接下来看看这一部分:fn(context, dispatch.bind(null, i + 1))
fn:当前中间件函数;
context:本次请求的上下文对象;
dispatch.bind(null, i + 1):下一个中间件的调用入口(并不是下一个中间件本身)。
下一个中间件的调用入口这一部分可能并不好理解,但是看看以下这个小例子大家就应该懂了。
下一个中间件的调用入口就是上图看到的next函数,是不是很熟悉?这下大家应该明白为什么koa中每个中间函数都会拿到下一个中间件的调用入口了。根据以上原理,中间件就构成了一个调用链。
由上可知,调用链执行完以后是返回一个promise对象,Koa最后还为我们定义了默认的处理函数handleResponse,它会在用户请求走完洋葱模型以后执行:
很多小伙伴就有一个问题了:为什么要定义handleResponse?这其实很简单,该函数是用来返回最后的处理结果,如果没有这个函数,用户的的请求就永远无法处理完成。那为什么handleResponse会在洋葱模型之后才执行呢?这就关联到JS事件循环的内容了,这里不展开讨论。
其实到这里还没有完,我们再来看看洋葱模型的图:
为什么会这样呢?我们来看下面两个图:
至此我们已经进一步了解了洋葱模型,但是还没有结束。。。。。为什么这么说呢?以上我们并没有考虑中间件函数中包含异步操作的情况!这里我门简单说一下如何处理异步的情况,我们需要借助ES6的async/await。
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx, next) => {
console.log(1)
await next() // 这里得到的就是中间件2返回的promise对象
console.log(3)}
)
app.use((ctx) => {
return new Promise((resolve,reject) => {
setTimeout(() => {
console.log(2) resolve()
}, 2000)
})
})
app.listen(3001)
async/await可将异步代码像同步一样去执行,保证了洋葱模型。至于为什么要保证洋葱模型,我简单说以下两个好处:
1、首先可以在最外层定义一个捕捉错误的中间件,提高代码的健壮性。
2、当我们需要通过ctx对象在中间件之间传递数据时,洋葱模型可以很好的保证数据的正确性。