理解Koa洋葱模型

1,051 阅读2分钟

koa中间件执行顺序

最近在学习koa中间件执行的原理,看下面代码:

import Koa from 'koa';
import fetch from 'node-fetch';

const app = new Koa();

app.use((ctx, next) => {
    console.log(1);
    next();
    console.log(1);
})
app.use((ctx, next) => {
    console.log(2);
    next();
    console.log(2);
})
app.use((ctx, next) => {
    console.log(3);
    next();
    console.log(3);
})

app.listen('3000', () => fetch('http:127.0.0.1:3000'))

猜猜输出结果? 是1, 1, 2, 2, 3, 3吗?,结果是

1
2
3
3
2
1

要理解这个行为,先理解koa的洋葱模型,app.use注册的中间件的回调就是按照这个模型来迭代的。

koa-compose.png

所以上面中间件回调是这样一个执行顺序:

koa-series.png

在执行回调的时候,如果调用了next()则进入下一个中间件回调,在如果某个中间件抛出了错误,则停止迭代。

koa中中间件的迭代实现是依赖了koa-compose这个包,源码很少,十分精简。

koa中间件执行原理

koa-compose.js


/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */
 
  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
       // 是否多次调用next();
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      // 从第一个中间件回调开始
      index = i
      // 取出回调
      let fn = middleware[i]
      // 已经迭代到最后一个注册的中间件,fn赋值为next
      if (i === middleware.length) fn = next
      // 如果next没有定义,结束,返回一个Promise
      // 如果定义了next回调,则执行next一次,因为compose函数可以单独使用
      // comopose([middleware])(context, () => console.log(6666))
      if (!fn) return Promise.resolve()
      try {
        // 执行中间件回调,并把dispatch作为第二个参数传next
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
       // 任何一个中间件跑出错误则停止迭代,返回一个失败的Promise
        return Promise.reject(err)
      }
    }
  }
}

其中compose入参middleware就是上面app.use注册的函数,

middleware: Function[] = [function, function, function];

上面主要的核心就是这行,把dispath作为async(ctx, next) {}的第二个参数传入,并通过bind绑定了参数i+1

Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

为什么要这样写dispatch.bind(null, i + 1), 主要是简洁,实际上你这样写也是ok的,最终都是一个闭包。

Promise.resolve(fn(context, () => dispatch.call(null, i + 1)));

调试koa-compose

只看代码,阅读还是比较难的。这里最好通过断点的方式来梳理执行顺序。 增加调试命令

{
  "scripts": {
    "debug": "node ./test/koa.js"
  }
}

然后就可以轻松断点调试了

debug.png