JavaScript is required
Blog About

通过koa中间件机制看组合函数

2025/02/27
6 mins read
See this issue
# Javascript
Back

# Koa洋葱圈模型

Koa的中间件核心实现是一个洋葱圈模型,用于将多个中间件函数按顺序串联执行,首先看一下Koa中关于中间件的组合函数部分的源码

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!')
    }


    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)
            }
        }
    }

单纯这样看还是比较复杂的,主要是中间组合的部分难以理解,为什么会组合形成一种洋葱的执行顺序

# 组合函数

函数式组合可以理解为将一系列简单基础函数组合成能完成复杂任务函数的过程; 这些基础函数都需要接受一个参数并且返回数据,这数据应该是另一个尚未可知的程序的输入

首先,我们先从一个最简单的组合函数开始

var compose = function(f,g) {
  return function(x) {
    return f(g(x));
  };
};

var toUpperCase = function(str) { return str.toUpperCase(); };
var exclaim = function(str) { return str + '!'; };
var shout = compose(exclaim, toUpperCase);
 
shout('hello world');
// HELLO WORLD!

在这个组合函数中传入了2个函数返回一个新函数,而新函数传入的是一个字符串和处理的结果,组合的核心是返回了一个闭包函数,真实参数是闭包函数的入参,真正的处理在闭包函数内

# mini-compose

接下来是一个实现了简单的中间件机制小型组合函数

function compose(middlewares) {
  return function (content) {
    let index = 0;

    function next() {
      if (index >= middlewares.length) return;
      const middleware = middlewares[index++];
      return middleware(content, next);
    }
    
    return next()
  }
}

const middlewares = [
  (ctx, next) => {
    console.log("1-Start");
    next();
    console.log("1-End");
  },
  (ctx, next) => {
    console.log("2-Start");
    next();
    console.log("2-End");
  },
];

const run = compose(middlewares);

run()

// 1-Start
// 2-Start
// 2-End
// 1-End

通过组合函数,实际上next就是下一个中间件,如果把递归展开,应该是形如下面

function run(ctx) {
  (ctx ) => {
    console.log("1-Start");
    ((ctx) => {
      console.log("2-Start");
      console.log("2-End");
    })()
    console.log("1-End");
  }()
}

执行过程图解

compose([m1, m2])()
│
▼
m1(context, next)        // 进入 m1
  │
  ▼
  console.log("1-Start") // 输出 1-Start
  │
  ▼
  next() → m2(context, next)  // 进入 m2
            │
            ▼
            console.log("2-Start") // 输出 2-Start
            │
            ▼
            next() → 无中间件,返回
            │
            ▼
            console.log("2-End") // 输出 2-End
  │
  ▼
  console.log("1-End") // 输出 1-End

这就是为什么koa的中间件可以实现洋葱顺序执行

如果想要异步也只需要稍作修改,对return的地方使用promise包装处理,就可以对next支持异步

function compose(middlewares) {
  return function (context) {
    let index = 0;

    // 返回 Promise 链
    function next() {
      if (index >= middlewares.length) return Promise.resolve();
      const middleware = middlewares[index++];
      // 将中间件执行结果包装为 Promise
      return Promise.resolve(middleware(context, next));
    }

    return next();
  };
}

const asyncMiddleware = async (ctx, next) => {
  console.log("Async Start");
  await next(); // 等待下一个中间件完成
  console.log("Async End");
};

对于异步中间件组合函数,在数据流程处理,框架设计方面是十分有用的设计。核心原理总结就是递归调用 + Promise链 + 闭包状态管理

# 参考文章