技术咨询、项目合作、广告投放、简历咨询、技术文档下载 点击这里 联系博主

# KOA 中间件 洋葱模型分析

在进行洋葱模型分析前先了解一下什么是洋葱模型; 比如有如下的代码:

const Koa = require("koa")
const app = new Koa();

// 中间件1
app.use(async (ctx, next) => {
    console.log("1")
    await next()
    console.log("2")
});
// 中间件2
app.use(async (ctx, next) => {
    console.log("3")
    await next()
    console.log("4")
});
// 中间件3
app.use(async (ctx, next) => {
    console.log("5")
    await next()
    console.log("6")
});
app.listen(8002);


那么代码执行的顺序应该是

1
3
5
6
4
2

通过可视化方式展示应该是这样的

起koa 实现 整个 中间件的核心是使用的 koa-compose (opens new window)库;

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
    function dispatch (i) {
      // 注意点3:防止一个中间件调用多个next函数
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      
      let fn = middleware[i]
      // 注意点2:防止数组超界
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      
      try {
        // 注意点1:递归调用下一个 对应的中间件函数
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
     
     // 开始调用第一个中间件函数
     return dispatch(0)

  }
}

上面提到三个注意点,我们逐步来分析一下:

# 注意点1: 使用 i + 1的方式 递归调用 下一个中间件函数

如果有如下的代码 那么

  • 首先会 执行 第一个中间件(dispatch(0));
  • 输出 log 1
  • 执行next: 因为next 是 dispatch.bind(null, i + 1) 所以其实执行的是第二个 中间件函数
  • 输出 log 3
  • 输出 log 4
  • 输出 log 2

const app = new Koa();
// 中间件1
app.use((ctx, next) => {
  console.log('1');
  next();
  console.log('2');
});
// 中间件2
app.use((ctx, next) => {
  console.log('3');
  console.log('4');
});

# 注意点2: 防止数组超界

如果存在如下代码;

  • 首先会 执行 第一个中间件(dispatch(0));
  • 输出 log 1
  • 执行 next: 因为next 是 dispatch.bind(null, i + 1) 所以其实执行的是第二个 中间件函数
  • 输出 log 3
  • 执行 next: 因为next 是 dispatch.bind(null, i + 1) 此时 i = 2; 但是现在一共只有 2个中间件,没有3个,所以 需要 加上 数组越界判断

const app = new Koa();
// 中间件1
app.use((ctx, next) => {
  console.log('1');
  next();
  console.log('2');
});
// 中间件2
app.use((ctx, next) => {
  console.log('3');
  next();
  console.log('4');
});

# 注意点3: 防止一个中间件调用多个next函数

通过分析如下的代码 我们来了解为什么 可以通过数组 下标去 防止多个next函数;

注意 index 的作用域 > i; 因为 i 是dispatch 的局部变量

  • 首先会 执行 第一个中间件(dispatch(0)): 此时 index = -1 , i = 0
  • 输出 log 1; 此时由于 index = i; 所以 index = 0; i = 0
  • 执行 next: 因为next 是 dispatch.bind(null, i + 1) 所以其实执行的是第二个 中间件函数: 此时 index = 0; i = 1
  • 输出 log 3; 此时由于 index = i; 所以 index = 1; i = 1
  • 执行 next: 因为next 是 dispatch.bind(null, i + 1) 所以其实执行的是第三个 中间件函数: 此时 index = 1; i = 2
  • 输出 log 5; 此时由于 index = i; 所以 index = 2; i = 2
  • 输出 log 6; 由于同log 5在同一个函数 所以 index = 2; i = 2

此时 index = i = 中间件的数量

由于 每一个中间件 执行 内部 的 i 是局部变量,依然保持着 0 ,1 , 2; 所以在执行 log 4 , log 2 之前 如果 还要 执行 next 则 i 分别 为 1 和 0

如果判断 i(1/0)<= index(2) 的话;就能有效的阻止 log 4 和 log 2 之前 再次调用 next 函数

const app = new Koa();
// 中间件1
app.use((ctx, next) => {
  console.log('1');
  next();
  next();
  console.log('2');
});
// 中间件2
app.use((ctx, next) => {
  console.log('3');
  next();
  console.log('4');
});
// 中间件3
app.use((ctx, next) => {
  console.log('5');
  console.log('6');
});

app.listen();

# 自定义compose实现 next 获取和return

function compose(middleware) {
    return function (_value, next) {
        let index = -1;
        
        function dispatch(i, value) {
            if (i <= index) throw new Error('next() called multiple times');
            index = i;
            const fn = middleware[i];
            if (i === middleware.length) fn = next;
            if (!fn) return (value);
            try {
                return (fn( value, (val) => dispatch(i + 1, val)));
            } catch (err) {
                return new Error(err);
            }
        }

        return dispatch(0, _value);
    };
}

const plugins = [
    function (value, next) {
        console.log(1, '===pre value===',value);//1 ===pre value=== 初始化的值
        const res = next('第1个函数传递的值');
        console.log(2, '===next value==', res);// 2 ===next value== { c: 3, b: 2 }
        return {
            ...res,
            a: 1
        };
    },
    function (value, next) {
        console.log(3, '===pre value===',value);
        const res = next('第2个函数传递的值'); // 3 ===pre value=== 第1个函数传递的值
        console.log(4, '===next value==', res); // 4 ===next value== { c: 3 }
        return {
            ...res,
            b: 2
        };
    },
    function (value, next) {
        console.log(5 ,'===pre value===',value);// 5 ===pre value=== 第2个函数传递的值
        return {
            c: 3
        };
    }
];

const func = compose(plugins);

// 执行函数,传入空对象作为 ctx 和一个空函数作为 next 的默认值
console.log(func('初始化的值'), '====res===');;


【未经作者允许禁止转载】 Last Updated: 1/16/2025, 12:47:53 PM