41

Koa源码系列之koa-compose

 4 years ago
source link: http://www.wclimb.site/2019/12/11/Koa源码系列之koa-compose/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

从今天开始阅读学习一下 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中间件处理流程,可以看下图

j6JVNv2.png!web

看下面代码是否能理解

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    |
        |          |            |                                 |            |           |
        |          |            +---------------------------------+            |           |
        |          |                                                           |           |
        |          +-----------------------------------------------------------+           |
        |                                                                                  |
        +----------------------------------------------------------------------------------+

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK