八月 26, 2019

Koa 的中间件完整流程 + 源码分析

Koa 的中间件原理总是一个被提问的问题,理论其实很简单,官方的洋葱模型其实就已经能够生动形象的解释中间件的运行机制了。但是,这么简单的解答又怎么能满足好奇心呢,所以这次,就又我来带大家走进 Koa 的中间件的世界(动物世界播音腔。

Koa 的中间件完整流程 + 源码分析

前言

Koa 的中间件原理总是一个被提问的问题,理论其实很简单,官方的洋葱模型其实就已经能够生动形象的解释中间件的运行机制了。但是,这么简单的解答又怎么能满足好奇心呢,所以这次,就又我来带大家走进 Koa 的中间件的世界(动物世界播音腔。

洋葱模型

源码

use()

在查看中间件合成的源码之前,我们还得还的知道我们是怎么将 middleware 传进去的,所以附上 koa 的 use() 源码:

use(fn) {
    // ...参数校验 fn 是否为普通函数
    
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
}

解读

这段其实很简单,感觉都不用附源码,但是在这里需要强调一个概念,middleware 本质上是个函数队列

koa-compose

Koa 中间件的实现并不在 Koa 的源码当中,而是在官网写的另一个库 koa-compose 当中。这种高解耦的方式,让每一个功能显得特小,所以 koa-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 是否为普通函数数组

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

解读

假设有一个 middelware = [m1, m2, m3, m4]

先返回的是 m1(content, dispatch(null, i + 1))content 利用了闭包,所以从始至终都是同一个。第二个参数实际上就是正常使用时的 nexti 是指向 middleware 数组的下标,每一个中间件函数的 next 实际上就是在调用下一个中间件函数,也就是说如果中间有中间件函数不调用 next,中间件就到此为止了。

简略来讲整个过程就是这样子的:

async m1(content, m2){
    // ...
    await m2(content, m3)
    // ...
}

async m2(content, m3){
    // ...
    await m2(content, m4)
    // ...
}

async m3(content, m4){
    // ...
    await m4(content, next)
    // ...
}

看完之后就会发现,洋葱模型确确实实已经生动形象的解释了整个中间件的运行过程。当然看了源码之后我们还可以理解一些错误是为什么不容许的,有些东西我们为什么要这么做,有些东西为什么会这样。

  • 中间件中不允许调用两次 next,多几次并不会运行后几个中间件函数,只会对运行几次下一个中间件函数,这是不必要的,也是不允许的。
  • 当下一个中间件实现完后会回头运行残留代码。
  • 真正的 next 会注入到最后一个中间件函数。
  • use 中间件的顺序就是中间件调用的顺序。
  • ...

callBack()

除此之外,我们为了了解中间件完整的运行流程,我们还可以了解到底谁用了中间件。 callBack() 是当服务创建时 http.createServer() 的参数 requestListener,是一个自动添加到 'request' 事件的函数,合成的中间件也是在此处引入的。

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

/**
 * Handle request in callback.
 *
 * @api private
 */

handleRequest(ctx, fnMiddleware) {
  const res = ctx.res;
  res.statusCode = 404;
  const onerror = err => ctx.onerror(err);
  const handleResponse = () => respond(ctx);
  onFinished(res, onerror);
  return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

解读

koa 先合成了中间件函数,然后再生成一个 content,将两个参数再交给 handleRequest 封装成一个用于处理请求的函数,最后返回给 node。然后每次请求都会经过这个函数来处理。

对比 Redux 的中间件实现

之所以要写这个环节,是为了拓展中间件这个概念,不要局限于 koa 这一地方,中间件是一个很优秀的编程思想,每种实现方式都有值得考究的地方,中间件的思想可以让一个流程性功能模块具有拓展性。

koa 的中间件拓展了处理 request 的过程,每一个中间件好比车间流水线上的一个个工作人员,只不过这个流水线是呈 U 字型的。

redux 的中间件拓展了 dispatch 的功能,利用 Array.prototype.reduce 将中间件首尾相连成一个管道,action 在这个管道穿梭、过滤,最后交给 redcuer 来处理。