Koa 洋葱模型的秘密 - koa-compose

我认识的 Koa

如果你接触过 node.js 的开发,肯定听说过 Express/Koa,其中 Koa 更是因为其轻量、优雅的中间件设计而成为经典的研发面试题目。

相信上面这张经典的图例,很多人都见到过,那么 Koa 中间件的设计(也即洋葱模型)是怎么实现的呢?带着疑问,开启这次源码共读之旅。

思考一下,下面代码执行结果

js 复制代码
const app = new Koa();

app.use(async (ctx, next) => {
	console.log(1);
	await next();
	console.log(6);
});

app.use(async (ctx, next) => {
	console.log(2);
	await next();
	console.log(5);
});

app.use(async (ctx, next) => {
	console.log(3);
	await next();
	console.log(4);
});

由上文洋葱模型的图例,我们可以得知,输出结果明显是:

shell 复制代码
1
2
3
4
5
6

这个是怎么实现的呢,能够让注册中间件从外到内执行,然后等待 next 执行后,能够逆序往外再执行剩下逻辑。

koa-compose

使以上洋葱模型生效的设计,就是 koa-compose 库;我们来看下它的源码实现

js 复制代码
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) {
      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)
      }
    }
    return dispatch(0)
  }
}

🥲 啊这,全部源代码就这点?是的,除了导出那行代码以外,这就是全部源代码!!!

从以上代码,可以看到 compose 函数接收的一个 middleware 的数组(实际上就是上文使用 app.use 注册的那些函数),数据的每一项都需要是函数类型。

那么我们就可以得知,洋葱模型的中间件实现秘密就是后面那段 return function (context, next) {...} 里面了。

js 复制代码
// 省略前面代码
return function (context, next) {
	// last called middleware #
	let index = -1
	function dispatch (i) {
	  if (i <= index) return Promise.reject(new Error('next() called multiple times'))
	  index = i
	  // 取出 middleware 数据里面的某一项
	  let fn = middleware[i]
	  // 最后到达末尾,next 值为 undefined
	  if (i === middleware.length) fn = next
	  if (!fn) return Promise.resolve()
	  try {
		// ??? 这段是怎么回事,递归 dispatch,bind ...
		return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
	  } catch (err) {
		return Promise.reject(err)
	  }
	}
	return dispatch(0)
}

dispatch 函数

上面那段难以理解的递归 dispatch.bind() 操作就是关键,口述比较难以解释,那么启用调试打发看看:

在上两图的代码处打上断点,然后访问,调试执行

我们选择 Step into,进入 fn 执行内部看看

没错,就是第一个中间件注册的地方,然后 await next() 执行后呢?

等等,执行回到原点?不过 i 变成了 1 ,断点到这里不得不感叹这短小精妙的代码竟是如此的优雅。这里的关键就是 dispatch.bind(null, i + 1) 这个高阶函数传参;bind 方法可以返回一个新的函数,且还可以把原函数接收的参数,提前传递到新返回的函数内 !(这是 bind 方法后面参数在起作用);至此,可以解释得通了,取出 middleware 当前项的函数执行后,传递的 next 方法实际上是下一个 middlware 项的函数再包装(使用 dispatch.bind),并且 dispatch 方法总是返回一个 Promise,因此 next 方法执行可以被 await 等待进入异步任务队列,且这里是调用了下一个中间件的函数 (栈结构)。栈结构的先进后出特性,实现从外到内执行,然后等待 next 执行后,能够逆序往外再执行剩下逻辑, 以上就是经典的"洋葱模型"个中细节和原理。

简化代码,加强理解

js 复制代码
// 剔除其余代码,简化后,这样更好理解一些
const [fn1, fn2, fn3] = middleware;
const fnMiddleware = function(context){
    return Promise.resolve(
      fn1(context, function next(){
        return Promise.resolve(
          fn2(context, function next(){
              return Promise.resolve(
                  fn3(context, function next(){
                    return Promise.resolve();
                  })
              )
          })
        )
    })
  );
};

当 middleware 到达最后一项是,返回一个 Promise.resolve() 纯净的 Promise 来作为 next,执行完之后就会逆序执行中间件 await next() 之后的代码逻辑了。

总结

koa-compose 源代码真的很精妙,有种大巧不工,浑然天成的感觉,真的666,但第一次看还是容易迷惑,使用断点调试大法理解起来会更加清楚、深刻,里面高阶函数、闭包、Promisebind等知识要灵活组合运用到一起,大大增加理解难度,学到了 😎。

相关推荐
Tans57 天前
LeakCanary 源码阅读笔记(四)
源码阅读·leakcanary
灵感__idea8 天前
Vuejs技术内幕:组件渲染
前端·vue.js·源码阅读
Sword9918 天前
【ThreeJs原理解析】第4期 | 向量
前端·three.js·源码阅读
程楠楠&M20 天前
koa中间件
前端·中间件·node.js·node·koa
biubiubiu王大锤24 天前
Nacos源码分析-永久实例健康检查机制
java·源码阅读
前端小臻1 个月前
后台管理-动态路由配置以及用户权限管理(vue3+element plus+koa+Sequelize )
前端·网络·node.js·koa
Sword991 个月前
【ThreeJs原理解析】第3期 | 射线检测Raycaster实现原理
前端·three.js·源码阅读
欧阳码农1 个月前
看不懂来打我!Vue3的watch是如何实现数据监听的
vue.js·源码·源码阅读
biubiubiu王大锤1 个月前
nacos源码分析-客户端启动与配置动态更新的实现细节
后端·源码阅读
Sword991 个月前
【ThreeJs原理解析】第2期 | 旋转、平移、缩放实现原理
前端·three.js·源码阅读