koa compose源码解析

koa compose 源码解析

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

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

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

源码解读前准备

了解洋葱模型

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

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.jsweb

执行结果以下所示:编程

为何会是上面这种结果呢,咱们带着这些疑问一块儿去继续往下看,看到最后确定会能理解。api

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

compose 源码解读

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

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 的解读和洋葱模型的解析。但愿对你们有所帮助,从代码上咱们能够看出,洋葱模型也是有所缺陷的,一旦中间件过多,性能仍是会有必定的影响的,因此咱们须要结合本身的项目场景做出合适的选择。

若是以上有问题,欢迎你们留言,一块儿探讨,谢谢!。

参考连接

相关文章
相关标签/搜索