从koa-compose中学习中间件洋葱模型

166 阅读4分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

1. 引言

koa-compose是一个专为Koa打造的中间件,按传入的中间件顺序返回一个最终中间件。整个koa-compose实现代码不到五十行,非常简洁,但其中包含的知识还是很多,理解起来有一定难度,我一般习惯先看一遍源码,大致理解下,然后对于无法理解的部分,通过测试用例了解代码做了什么,然后再调试理解是怎么做的。文中如果存在一些表述不清或出错的地方,还望各位读者大大指出,不吝感激。

2. 源码分析

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)
       }
     }
   }
 }
  • 首先是传入参数的校验Array.isArray()传入的是否为数组,否直接抛出错误
  • 遍历数组,判断数组所有元素是否都为函数,使用typeof进行判断
  • 返回一个函数中间件,中间件包含两个固定参数context上下文和next执行下一中间件,查看Koa官方文档了解到这两个参数应该是由app.use()调用中间件时传入,但这里还有个疑惑,就是next()执行下一个是如何实现?
  • 返回的中间件执行dispatch方法调用

dispatch

 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)
   }
 }
  • 判断i小于等于index时,抛出错误next()函数被多次调用

  • 取到传入中间件数组的第i

  • 判断当i等于中间件数组的长度暨已经取完传入所有中间件时,将fn设置为next

  • 判断如果fnundefined暨正常执行完最后一个中间件,怎整个函数正常返回

  • 否则,递归执行下一个中间件fn,当任何一个中间件执行出错时,都会直接抛出错误

  • 这里还有几个有疑问的点:

    1. if (i === middleware.length) fn = next这一句是有什么考量,感觉上和下一句判断重复了?
    2. 这个next和中间件中调用的next()是同一个吗?
    3. 最后就是虽然每行都能理解个十之八九,但整体执行的过程还是有点懵?下面带着疑问和迷惑单步调试下测试用例,看看能不能找出答案

调试

对于代码调试的方法这里就不多说了,有不懂可参看若川大佬的这篇文章:新手向:前端程序员必学基本技能——调试JS代码

zhixing.png

  • 调试运行,停在我设置的入口断点,及compose函数调用的位置,可以看出函数接受一个函数中间件数组stack

compose.png

  • 单步执行,进入compose函数,参数判断就不看了,这里肯定是满足条件的

return.png

  • 执行到return,直接返回包装后的函数中间件
  • 因为测试用例中的定义compose的返回值是自执行的,所以会马上执行到返回的函数

MiddleWare.png

  • 执行返回的中间件,继续执行dispatch函数

middleWareOne.png

one.png

  • 执行第一个中间件
  • 此处可以看到中间件执行的next()既是执行dispatch(i + 1)暨执行到下一个中间件;因此前面的两个疑问得到了解决,next()的实现原理以及next()都是同一个,暨都是执行下一中间件

next.png

  • 执行next()下一中间件

OK,调试到这里我基本上理解了这个执行逻辑了。

至此,已经有了一个compose处理过后函数调用的基本雏形了

 const stack = [fn1, fn2, fn3];
 ​
 function compose(stack) {
     return 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();
                             })
                         )
                     })
                 )
             })
         )
     };
 }

根据传入的中间件的顺序依次向内执行,之后再依次向外返回结果

4. 总结

这期跟随这若川大佬的部分,结合koa-compose的源码,浅显的理解了下关于koa中中间件的洋葱模型;通过这期的源码学习,不仅仅时理解到了中间件洋葱模型的实现原理,也是开拓了自己的眼界,看完这简简单单不到五十行代码之后才会发现原来代码也能写得这么惊艳,这么简洁,只能说是叹为观止,自己所要走的路还很远呀。