koa compose源码解析

3,539 阅读4分钟

koa compose 源码解析

koa -基于 Node.js 的下一代 web 开发框架。

它最大的特点就是独特的中间件流程控制,是一个典型的洋葱模型。koa 和 koa2 中间件的思路是一样的,但是实现方式有所区别,koa2 在 Node7.6 之后更是可以直接用 async/await 来替代 generator 使用中间件,本文以最后一种情况举例。

本文主要是对 compose 模块的源码解读

源码解读前准备

了解洋葱模型

下面的图是网上找的,很清晰的表明了一个请求是如何经过中间件最后生成响应的,这种模式中开发和使用中间件都是非常方便的。 我们都知道在函数式编程的思想中,compose 是将多个函数合并成一个函数(g() + h() => g(h())),koa 中的 compose 则是将 koa/koa-router 各个中间件合并执行,结合 next() 就形成了下图所示的洋葱模型

koa 示例测试,查看洋葱模型的执行顺序和优点

  • 执行顺序测试 我们创建一个 koa 应用
const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
    console.log('第一个中间件函数')
    await next();
    console.log('第一个中间件函数next之后!');
})
app.use(async (ctx, next) => {
    console.log('第二个中间件函数')
    await next();
    console.log('第二个中间件函数next之后!');
})

app.use(async ctx => {
    ctx.body = 'Hello World';
});

app.listen(3000);   

执行命令 node demo1.js

执行结果如下所示:

为什么会是上面这种结果呢,我们带着这些疑问一起去继续往下看,看到最后肯定会能理解。

注意:在使用app.use将给定的中间件添加到应用程序时,middlewar(其实就是一个函数)接收两个参数:ctxnext。其中next也是一个函数。

compose 源码解读

compose 代码如下,去掉注释,代码就 25 行,细读确实是很精妙的代码,虽然看着很短,但粗看几层 return ,还是会有点绕。

module.exports = 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) {
    //传入的 middleware 参数必须是数组
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  //middleware 数组的元素必须是函数
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise} 返回一个闭包函数,函数的返回是一个Promise 对象, 保持对 middleware 的引用。
   * @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)
      }
    }
  }
}

我们首先去掉条件判断,看下最里面的实际返回

return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

fn = middleware[i] 也就是某一个中间件,很显然上述代码遍历中间件数组middleware,依次拿到中间件fn,并执行:

fn(context, dispatch.bind(null, i + 1))

这里可以看到传递给中间件的两个参数:contextnext函数。 前面我们提到过:在使用app.use 将定的中间件添加到应用程序时,中间件(其实就是一个函数)接收两个参数:ctxnext。其中next也是一个函数。 看到这里是不是清楚了在注册 middwleare的时候为什么要有两个参数了吧~

回到前面的问题,为什么我们的demo执行的结果会是上面,我们看第一个中间件,

app.use(async (ctx, next) => {
    console.log('第一个中间件函数')
    await next();
    console.log('第一个中间件函数next之后!');
})

带入到代码中,第一次执行 return dispatch(0), 这时第一个中间件被调用,继续展开

  • dispatch(0)展开
Promise.resolve((async (ctx, next) => {
   console.log('第一个中间件函数')
   await next();
   console.log('第一个中间件函数next之后');
})(context, dispatch.bind(null, i + 1)));

首先执行 console.log('第一个中间件函数')没啥毛病, 接下来执行 next()方法,就跑到第二个中间件去了,所以没有执行第二个 console.log()

app.use(async (ctx, next) => {
    console.log('第二个中间件函数')
    await next();
    console.log('第二个中间件函数next之后!');
})
  • dispatch(1)展开
Promise.resolve(async (ctx, next) => Promise.resolve(async (ctx, next) => {
    console.log('第一个中间件函数')
    Promise.resolve((async (ctx, next) => {
        console.log('第二个中间件函数')
        await next();
        console.log('第二个中间件函数next之后');
    })(context, dispatch.bind(null, i + 1)));
    console.log('第一个中间件函数next之后')
}))

所以执行 onsole.log('第二个中间件函数')是不是就很清楚的看出来了。

在第二个中间件执行到await next()时,同样会轮转到第三个中间件,接下如果有第四个中间件,第五个中间件,聪明的你们会发现,以此类推,直到最后一个中间件。

看到这里,我们会不会很好奇 koa 是怎么调用compose 的呢,等后面的文章再更新~

总结

以上就是我关于 koa compose 的解读和洋葱模型的解析。希望对大家有所帮助,从代码上我们可以看出,洋葱模型也是有所缺陷的,一旦中间件过多,性能还是会有一定的影响的,所以我们需要结合自己的项目场景作出合适的选择。

如果以上有问题,欢迎大家留言,一起探讨,谢谢!。

参考链接