Koa2 In-depth Analysis


Koa2 In-depth Analysis

koa核心主要是context(上下文)和middleware(中间件)组成,v2的中间件部分可以使用ES2015-2016的语法,比如async await和箭头函数,同时支持3种不同种类的中间件,普通函数,async 函数,Generator函数。先看如何使用:

普通函数的用法

app.use((ctx, next) => {
  const start = new Date();
  return next().then(() => {
    const ms = new Date() - start;
    console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
  });
});

async函数的用法

app.use(async (ctx, next) => {
  const start = new Date();
  await next();
  const ms = new Date() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

Generator函数的两种用法

app.use(co.wrap(function *(ctx, next) {
  const start = new Date();
  yield next();
  const ms = new Date() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
}));
const convert = require('koa-convert');
app.use(convert(function *(next) {
  const start = new Date();
  yield next;
  const ms = new Date() - start;
  console.log(`${this.method} ${this.url} - ${ms}ms`);
}));

直接用v1的语法也可以,像下面这样

app.use(function *(next) {
  const start = new Date();
  yield next;
  const ms = new Date() - start;
  console.log(`${this.method} ${this.url} - ${ms}ms`);
});

因为同时支持3种类型的中间件,所以这块改动比较大,先从注册中间件开始,也就是app.use

use(fn) {
  if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
  if (isGeneratorFunction(fn)) {
    fn = convert(fn);
  }
  this.middleware.push(fn);
  return this;
}

v2多了一个判断,如果是Generator函数,那就用 convert 把函数包起来,然后在push到this.middleware 这就是针对v1的写法做的兼容。或者自己把v1的中间件用 convert 包起来在use。convert源码:

function convert (mw) {
  if (typeof mw !== 'function') {
    throw new TypeError('middleware must be a function')
  }
  if (mw.constructor.name !== 'GeneratorFunction') {
    // assume it's Promise-based middleware
    return mw
  }
  const converted = function (ctx, next) {
    return co.call(ctx, mw.call(ctx, createGenerator(next)))
  }
  converted._name = mw._name || mw.name
  return converted
}

也就是利用co来wrap这个generator,返回promise,具体参考co源码

中间件运行

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

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

  return (req, res) => {
    res.statusCode = 404;
    const ctx = this.createContext(req, res);
    onFinished(res, ctx.onerror);
    fn(ctx).then(() => respond(ctx)).catch(ctx.onerror);
  };
}

看第一行代码

const fn = compose(this.middleware);

现在 this.middleware 中存了一些函数,只要执行它就返回promise。这个函数有可能是 async 函数 有可能是被 convert 包装后的Generator函数,或者是被 co.wrap 包装后的Generator函数,也有可能是普通函数的中间件,反正这些函数都有一个特性,那就是执行它们,会统一返回promise。看看compose是怎么把三个种类的中间件变成可以实现中间件逻辑的函数呢?

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
      const fn = middleware[i] || next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
  fn(ctx).then(() => respond(ctx)).catch(ctx.onerror);

看具体过程

首先在执行到匿名函数的时候(就是return返回的那个函数),会执行 dispatch,并传一个参数 0,其次就是在 dispatch 执行的过程中会自己调用自己,递归调用。i 其实是用来在 this.middleware 中获取中间件的下标.这行代码获取中间件const fn = middleware[i] || next,取到中间件之后

return Promise.resolve(fn(context, function next () {
  return dispatch(i + 1)
}))

执行中间件并传递两个参数,context 和 next函数,context是koa中的上下文,next就是return一个dispatch 的执行结果,注意那个参数 i+1,传递一个 i+1,就相当于一旦执行next函数,就等同于执行下一个中间件。

一个中间件只能执行一次next,否则逻辑上会出现问题,为了避免这个问题,在 dispatch 中一开始就做了判断,就是一开始index和i的比较。

在中间件中,我们通常会这样使用await next();async的语法是,await后面会跟一个promise,await会等待promise,等promise执行完了,在往下执行.而我们的这些中间件,都有一个特点,执行完会返回promise,所以正好被await监听。 我们中间件本身返回的就是promise,为什么会被Promise.resolve包起来?这里是一个兼容写法,如果只支持async函数当然没问题,但我们的中间件除了支持async函数外,还支持普通函数.如果中间件使用async函数写的,流程大概是这样的:

  1. 先执行第一个中间件(因为默认会先执行一次dispatch(0)),这个中间件会返回promise,koa会监听这个promise,一旦成功或者失败,都会做出不同的处理,并结束这次响应

  2. 在执行中间件逻辑的时候,我们会执行这样一段代码 await next();,在这里手动触发第二个中间件执行,第二个中间件和第一个中间件一样,也会返回promise.await会监听这个promise,什么时候执行完了,什么时候继续执行第一个中间件后续的代码。(中间件的回逆就是这样实现的)

  3. 在第二个中间件触发的时候,也会执行 await next(); 这样一段代码来触发第三个中间件并等待第三个中间件执行完了在执行后续代码,否则就一直等,以此类推 所以就造成了这样一个现象,第一个中间件代码执行一半停在这了,触发了第二个中间件的执行,第二个中间件执行了一半停在这了,触发了第三个中间件的执行,然后,,,,,,第一个中间件等第二个中间件,第二个中间件等第三个中间件,,,,,,第三个中间件全部执行完毕,第二个中间件继续执行后续代码,第二个中间件代码全部执行完毕,执行第一个中间件后续代码,然后结束.

messge flow

async函数配合中间件使用的条件

  1. 执行后返回promise
  2. 函数内部可以通过await暂停函数,并等待下一个中间件执行完成后,继续执行

中间件的实现逻辑 - 普通函数

首先我们看下普通函数的用法

app.use((ctx, next) => {
  const start = new Date();
  return next().then(() => {
    const ms = new Date() - start;
    console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
  });
});

先说第一个条件,「执行后返回promise」上面我们说过,我们的中间件在 dispatch 中会被 Promise.resolve 包住并返回,所以第一个条件满足.再说第二个条件,「中间件内部可以监听promise并等待promise接收后执行后续代码」很明显,第二个条件也满足,因为普通函数的写法是异步的,后续代码在then里面。(async也不过是看起来同步而已,其实是同样的逻辑)

中间件的实现逻辑 - Generator函数

先看看用法

app.use(co.wrap(function *(ctx, next) {
  const start = new Date();
  yield next();
  const ms = new Date() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
}));

首先第一个条件「执行后返回promise」可以看到中间件是用 co.wrap 包起来的,co.wrap会返回promise,参考co的源码,第一个条件满足.再说第二个条件,「中间件内部可以监听promise并等待promise接收后执行后续代码」co 与async一样,yield后面可以跟一个promise,co会监听这个promise,什么时候这个promise执行完了。什么时候执行后续的代码,这点跟async是一模一样的,只是写法略有不同,第二个条件也满足.