从源码角度分析Koa2.0中间件机制

中间件概念

在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模块的源代码:

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):下一个中间件的调用入口(并不是下一个中间件本身)。

下一个中间件的调用入口这一部分可能并不好理解,但是看看以下这个小例子大家就应该懂了。

koa项目案例

下一个中间件的调用入口就是上图看到的next函数,是不是很熟悉?这下大家应该明白为什么koa中每个中间函数都会拿到下一个中间件的调用入口了。根据以上原理,中间件就构成了一个调用链。

由上可知,调用链执行完以后是返回一个promise对象,Koa最后还为我们定义了默认的处理函数handleResponse,它会在用户请求走完洋葱模型以后执行:

业务处理函数

很多小伙伴就有一个问题了:为什么要定义handleResponse?这其实很简单,该函数是用来返回最后的处理结果,如果没有这个函数,用户的的请求就永远无法处理完成。那为什么handleResponse会在洋葱模型之后才执行呢?这就关联到JS事件循环的内容了,这里不展开讨论。


其实到这里还没有完,我们再来看看洋葱模型的图:

洋葱模型


为什么会这样呢?我们来看下面两个图:

中间件执行顺序图1
中间件执行顺序图2


至此我们已经进一步了解了洋葱模型,但是还没有结束。。。。。为什么这么说呢?以上我们并没有考虑中间件函数中包含异步操作的情况!这里我门简单说一下如何处理异步的情况,我们需要借助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对象在中间件之间传递数据时,洋葱模型可以很好的保证数据的正确性。

你可能感兴趣的:(从源码角度分析Koa2.0中间件机制)