深入理解 Koa2 中间件机制

我们知道,Koa 中间件是以级联代码(Cascading) 的方式来执行的。类似于回形针的方式,可参照下面这张图:

今天这篇文章就来分析 Koa 的中间件是如何实现级联执行的。 在 koa 中,要应用一个中间件,我们使用 app.use():

先来看看use() 是什么,它的源码如下: use(fn) {

    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');

    if (isGeneratorFunction(fn)) {

      deprecate('Support for generators will be removed in v3. ' +

                'See the documentation for examples of how to convert old middleware ' +

                'https://github.com/koajs/koa/blob/master/docs/migration.md');

      fn = convert(fn);

    }

    debug('use %s', fn._name || fn.name || '-');

    this.middleware.push(fn);

    return this;

  }

这个函数的作用在于将调用use(fn)方法中的参数(不管是普通的函数或者是中间件)都添加到this.middlware这个数组中。

在Koa2中,还对Generator语法的中间件做了兼容,使用isGeneratorFunction(fn)这个方法来判断是否为Generator语法,并通过convert(fn)这个方法进行了转换,转换成async/await语法。然后把所有的中间件都添加到了this.middleware,最后通过callback()这个方法执行。callback() 源码如下:

源码中,通过compose()这个方法,就能将我们传入的中间件数组转换并级联执行,最后callback()返回this.handleRequest()的执行结果。返回的是什么内容我们暂且不关心,我们先来看看compose()这个方法做了什么事情,能使得传入的中间件能够级联执行,并返回Promise。

compose()是 koa2 实现中间件级联调用的一个库,叫做koa-compose。源码很简单,只有一个函数,如下:

可以看到compose()返回一个匿名函数的结果,该匿名函数自执行了dispatch()这个函数,并传入了0作为参数。

来看看dispatch(i)这个函数都做了什么事?i作为该函数的参数,用于获取到当前下标的中间件。在上面的dispatch(0)传入了0,用于获取middleware[0]中间件。

首先显示判断i<==index,如果true的话,则说明next()方法调用多次。为什么可以这么判断呢?等我们解释了所有的逻辑后再来回答这个问题。

接下来将当前的i赋值给index,记录当前执行中间件的下标,并对fn进行赋值,获得中间件

获得中间件后,怎么使用?

上面的代码执行了中间件fn(context, next),并传递了context和next函数两个参数。context就是koa中的上下文对象context。至于next函数则是返回一个dispatch(i+1)的执行结果。值得一提的是i+1这个参数,传递这个参数就相当于执行了下一个中间件,从而形成递归调用。这也就是为什么我们在自己写中间件的时候,需要手动执行

await next()

只有执行了next函数,才能正确得执行下一个中间件。

因此每个中间件只能执行一次next,如果在一个中间件内多次执行next,就会出现问题。回到前面说的那个问题,为什么说通过i<=index就可以判断next执行多次?

因为正常情况下index必定会小于等于i。如果在一个中间件中调用多次next,会导致多次执行dispatch(i+1)。从代码上来看,每个中间件都有属于自己的一个闭包作用域,同一个中间件的i是不变的,而index是在闭包作用域外面的。

当第一个中间件即dispatch(0)的next()调用时,此时应该是执行dispatch(1),在执行到下面这个判断的时候,

if (i <= index) return Promise.reject(new Error('next() called multiple times'))

此时的index的值是0,而i的值是1,不满足i<=index这个条件,继续执行下面的index=i的赋值,此时index的值为1。但是如果第一个中间件内部又多执行了一次next()的话,此时又会执行dispatch(2)。上面说到,同一个中间件内的i的值是不变的,所以此时i的值依然是1,所以导致了i <= index的情况。

可能会有人有疑问?既然async本身返回的就是Promise,为什么还要在使用Promise.resolve()包一层呢。这是为了兼容普通函数,使得普通函数也能正常使用。

再回到中间件的执行机制,来看看具体是怎么回事。我们知道async的执行机制是:只有当所有的await异步都执行完之后才能返回一个Promise。所以当我们用async的语法写中间件的时候,执行流程大致如下:

先执行第一个中间件(因为compose会默认执行dispatch(0)),该中间件返回Promise,然后被Koa监听,执行对应的逻辑(成功或失败)

在执行第一个中间件的逻辑时,遇到await next()时,会继续执行dispatch(i+1),也就是执行dispatch(1),会手动触发执行第二个中间件。这时候,第一个中间件await next()后面的代码就会被 pending,等待await next()返回Promise,才会继续执行第一个中间件await next()后面的代码。

同样的在执行第二个中间件的时候,遇到await next()的时候,会手动执行第三个中间件,await next()后面的代码依然被 pending,等待await下一个中间件的Promise.resolve。只有在接收到第三个中间件的resolve后才会执行后面的代码,然后第二个中间会返回Promise,被第一个中间件的await捕获,这时候才会执行第一个中间件的后续代码,然后再返回Promise

以此类推,如果有多个中间件的时候,会依照上面的逻辑不断执行,先执行第一个中间件,在await next()出 pending,继续执行第二个中间件,继续在await next()出 pending,继续执行第三个中间,直到最后一个中间件执行完,然后返回 Promise,然后倒数第二个中间件才执行后续的代码并返回Promise,然后是倒数第三个中间件,接着一直以这种方式执行直到第一个中间件执行完,并返回Promise,从而实现文章开头那张图的执行顺序。

高级java免费课程:https://ke.qq.com/course/131889?flowToken=1006149全套java资料领取QQ: 514683544

你可能感兴趣的:(深入理解 Koa2 中间件机制)