Koa源码系列之koa-compose

从今天开始阅读学习一下 Koa 源码, Koa 对前端来说肯定不陌生,使用 node 做后台大部分会选择 Koa 来做, Koa 源码的代码量其实很少,接下来让我们一层层剥离,分析其中的源码

Koa用法

const Koa = require("koa");
const app = new Koa();
app.use(async ctx => {
  ctx.body = {
    a: 1
  };
});
app.listen(3000);

浏览器打开 http://localhost:3000 可以看到返回了一个对象 {a:1}

洋葱模型

Koa最经典的就是基于洋葱模型的HTTP中间件处理流程,可以看下图

看下面代码是否能理解

const Koa = require("koa");
const app = new Koa();
app.use(async (ctx,next) => {
  console.log(1);
  setTimeout(()=>{
    next()
    console.log(2)
  },1000)
});
app.use(async (ctx,next) => {
  console.log(3);
  next()
  console.log(4)
});
app.use(async (ctx,next) => {
  console.log(5);
  setTimeout(()=>{
    console.log(6)
  },1000)
  next()
  console.log(7)
});
app.listen(3000);

访问 http://localhost:3000 输出

1
3  // 1秒后开始输出
5
7
4
2
6 // 1秒后开始输出

不知道看到这你是否能够明白,不明白也没关系,我们可以深入源码来分析具体的实现

koa源码

https://github.com/koajs/koa/blob/master/lib/application.js#L77

执行 app.listen(3000) ;会走以下代码

/**
 * Shorthand for:
 *
 *    http.createServer(app.callback()).listen(...)
 *
 * @param {Mixed} ...
 * @return {Server}
 * @api public
 */

listen(...args) {
  debug('listen');
  const server = http.createServer(this.callback());
  return server.listen(...args);
}、

https://github.com/koajs/koa/blob/master/lib/application.js#L141

我们接着看 callback 方法

/**
 * Return a request handler callback
 * for node's native http server.
 *
 * @return {Function}
 * @api public
 */

callback() {
  const fn = compose(this.middleware);

  if (!this.listenerCount('error')) this.on('error', this.onerror);

  const handleRequest = (req, res) => {
    const ctx = this.createContext(req, res);
    return this.handleRequest(ctx, fn);
  };

  return handleRequest;
}

在上面 callback 执行了 compose 方法来处理中间件, compose 方法就是今天我们需要重点讲的方法,等会再说,我们知道 Koa 大部分情况都是在处理中间件,那么它们是怎么拿到中间件的呢?

上面可以看到有一个 this.middleware ,中间件肯定是放在这里面的,于是我们搜索,在最上层可以在构造函数里看到初始化的时候,它把 this.middleware = [] ;置为一个数组,我们使用中间件的时候是通过 app.use 来使用的。继续寻找 use 方法

https://github.com/koajs/koa/blob/master/lib/application.js#L120

use 方法

/**
 * Use the given middleware `fn`.
 *
 * Old-style middleware will be converted.
 *
 * @param {Function} fn
 * @return {Application} self
 * @api public
 */

use(fn) {
  if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
  if (isGeneratorFunction(fn)) {
    deprecate('Support for generators will be removed in v3. ' +
              'See the documentation for examples of how to convert old middleware ' +
              'https://github.com/koajs/koa/blob/master/docs/migration.md');
    fn = convert(fn);
  }
  debug('use %s', fn._name || fn.name || '-');
  this.middleware.push(fn);
  return this;
}

每次我们调用 app.usekoa 都是把这个方法 pushmiddleware 数组里面。感觉说得有点啰嗦了,流程大体就是这样。

定义中间件数组 -> 收集中间件放到middleware数组里 -> 通过compose方法处理中间件达到洋葱模式

koa-compose

https://github.com/koajs/compose/blob/master/index.js

compose 是引用的 koa-compose 包,查看源码发现关键只有20几行

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

我们默许中间件传入的是数组函数,进一步剥离,精简的代码如下

function compose (middleware) {
  return function (context, next) {
    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)
      }
    }
  }
}

初略的看, compose 返回的是一个函数,首先执行了 dispatch(0) ,函数内容返回的都是Promise对象。

最初执行 dispatch(0) ;通过递归的方式不断的运行中间件,每个中间件都接收了两个参数,分别是 contextnextcontext 其实就是 koactx ,如果我们不传递 next 方法,后面的中间件就不会继续执行下去。

之前的代码我们可以初略的想象一下以下代码

function fn1(context, next) {
  console.log(1);
  next();
  console.log(2);
}

function fn2(context, next) {
  console.log(3);
  next();
  console.log(4);
}

function fn3(context, next) {
  console.log(5);
  next();
  console.log(6);
}
function compose() {
  return fn1('', () => {
    return fn2('', () => {
      return fn3('', () => {
      });
    });
  });
}

输出:

其实就是一层层嵌套执行中间件的方法,执行完 next 再往上执行

       -------------------------------------------------------------------------------------
        |                                                                                  |
        |                                       fn1                                        |
        |          +-----------------------------------------------------------+           |
        |          |                                                           |           |    
        |          |                            fn2                            |           |
        |          |            +---------------------------------+            |           |
        |          |            |                                 |            |           |
        |          |            |               fn3               |            |           |
        |          |            |                                 |            |           |
---------------------------------------------------------------------------------------------------->
        |          |            |                                 |            |           |
        |     1    |      3     |       5                 6       |     4      |      2    |
        |          |            |                                 |            |           |
        |          |            +---------------------------------+            |           |
        |          |                                                           |           |
        |          +-----------------------------------------------------------+           |
        |                                                                                  |
        +----------------------------------------------------------------------------------+