深入浅出洋葱模型

72 阅读2分钟

洋葱模型

定义:以next函数为分割点,先由外到里执行request的逻辑,再由里到外执行response的逻辑。也就是说,每一个中间件都会有两次处理时机。

举个例子

const app = new Koa();

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

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

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

// 执行后,控制台打印应该是
1
2
3
3-3
2-2
1-1

在koa中,中间件被next分割成2部分。next上面的部分会被先执行,下面部分会在中间件全部执行结束后再执行,如下图所示:

洋葱模型

在洋葱模型当中,每一层相当于一个中间件,用来处理特定的功能,例如错误抓去、session处理。

中间件处理

koa 的实现

use 方法

use 方法只干一件事,就是将中间件函数压入需要执行的数组当中。

function use(fn) {
  this.middleware.push(fn);
  return this;
}

compose 方法

compose 方法需要将压入完成的中间件数组,通过递归的方式组合执行顺序。先看下建议的实现逻辑

function compose(middlewares) {
  return (ctx, next) => {
    function dispatch(i) {
      const fn = middleware[i];
      if (!fn) {
        return;
      }

      return fn(ctx, dispatch.bind(null, i + 1));
    }

    return dispatch[0];        
  }
}
  1. 执行middleware1,执行到next函数
  2. 执行middleware2,执行到next函数
  3. 执行middles3,执行到next函数,发现没有下一个中间件,跳过,继续执行
  4. middleware3执行完毕,即middleware2的next函数执行完毕,继续执行middleware2的剩余代码
  5. middleware2执行完毕,即middleware1的next函数执行完毕,继续执行middleware1的剩余代码

其实最关键的理解点就是 next 函数就是下一个要执行的中间件!

koa的源码实现

koa是通过这个库 koa-compose 来实现的

    /**
     * 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) {
          if (i <= index) return Promise.reject(new Error('next() called multiple times'))
          index = i
          let fn = middleware[i]
          if (i === middleware.length) fn = next
          if (!fn) return Promise.resolve()
          try {
            return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
          } catch (err) {
            return Promise.reject(err)
          }
        }
      }
    }