koa2中间件_深入理解 Koa2 中间件机制

更新日期: 2018-11-10阅读: 3.5k标签: koa

我们知道,Koa 中间件是以级联代码(Cascading) 的方式来执行的。类似于回形针的方式,可参照下面这张图:


今天这篇文章就来分析 Koa 的中间件是如何实现级联执行的。
在 koa 中,要应用一个中间件,我们使用 app.use():

app
  .use(logger())
  .use(bodyParser())
  .use(helmet())

先来看看use() 是什么,它的源码如下:

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

这个函数的作用在于将调用 use(fn) 方法中的参数(不管是普通的函数或者是中间件)都添加到 this.middlware 这个数组中。

在 Koa2 中,还对 Generator 语法的中间件做了兼容,使用 isGeneratorFunction(fn) 这个方法来判断是否为 Generator 语法,并通过 convert(fn) 这个方法进行了转换,转换成 async/await 语法。然后把所有的中间件都添加到了 this.middleware ,最后通过 callback() 这个方法执行。callback() 源码如下:

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

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

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

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

    return handleRequest;
  }

源码中,通过 compose() 这个方法,就能将我们传入的中间件数组转换并级联执行,最后 callback() 返回this.handleRequest()的执行结果。返回的是什么内容我们暂且不关心,我们先来看看 compose() 这个方法做了什么事情,能使得传入的中间件能够级联执行,并返回 Promise。

compose() 是 koa2 实现中间件级联调用的一个库,叫做 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) {
  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) {
    // 记录上一次执行中间件的位置 #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      // 理论上 i 会大于 index,因为每次执行一次都会把 i递增,
      // 如果相等或者小于,则说明next()执行了多次
      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, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

可以看到 compose() 返回一个匿名函数的结果,该匿名函数自执行了 dispatch() 这个函数,并传入了0作为参数。

来看看 dispatch(i) 这个函数都做了什么事?
i 作为该函数的参数,用于获取到当前下标的中间件。在上面的 dispatch(0) 传入了0,用于获取 middleware[0] 中间件。

首先显示判断 i<==index,如果 true 的话,则说明 next() 方法调用多次。为什么可以这么判断呢?等我们解释了所有的逻辑后再来回答这个问题。

接下来将当前的 i 赋值给 index,记录当前执行中间件的下标,并对 fn 进行赋值,获得中间件。

index = i;
let fn = middleware[i]

获得中间件后,怎么使用?

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

上面的代码执行了中间件 fn(context, next),并传递了 context 和 next 函数两个参数。context 就是 koa 中的上下文对象 context。至于 next 函数则是返回一个 dispatch(i+1) 的执行结果。值得一提的是 i+1 这个参数,传递这个参数就相当于执行了下一个中间件,从而形成递归调用。
这也就是为什么我们在自己写中间件的时候,需要手动执行

await next()

只有执行了 next 函数,才能正确得执行下一个中间件。

因此每个中间件只能执行一次 next,如果在一个中间件内多次执行 next,就会出现问题。回到前面说的那个问题,为什么说通过 i<=index 就可以判断 next 执行多次?

因为正常情况下 index 必定会小于等于 i。如果在一个中间件中调用多次 next,会导致多次执行 dispatch(i+1)。从代码上来看,每个中间件都有属于自己的一个闭包作用域,同一个中间件的 i 是不变的,而 index 是在闭包作用域外面的。

当第一个中间件即 dispatch(0) 的 next() 调用时,此时应该是执行 dispatch(1),在执行到下面这个判断的时候,

if (i <= index) return Promise.reject(new Error('next() called multiple times'))

此时的 index的值是0,而 i 的值是1,不满足 i<=index 这个条件,继续执行下面的 index=i 的赋值,此时 index 的值为1。但是如果第一个中间件内部又多执行了一次 next()的话,此时又会执行 dispatch(2)。上面说到,同一个中间件内的 i 的值是不变的,所以此时 i 的值依然是1,所以导致了 i <= index 的情况。

可能会有人有疑问?既然 async 本身返回的就是 Promise,为什么还要在使用 Promise.resolve() 包一层呢。这是为了兼容普通函数,使得普通函数也能正常使用。

再回到中间件的执行机制,来看看具体是怎么回事。
我们知道 async 的执行机制是:只有当所有的 await 异步都执行完之后才能返回一个 Promise。所以当我们用 async 的语法写中间件的时候,执行流程大致如下:

  1. 先执行第一个中间件(因为compose 会默认执行 dispatch(0)),该中间件返回 Promise,然后被 Koa 监听,执行对应的逻辑(成功或失败)
  2. 在执行第一个中间件的逻辑时,遇到 await next()时,会继续执行 dispatch(i+1),也就是执行 dispatch(1),会手动触发执行第二个中间件。这时候,第一个中间件 await next() 后面的代码就会被 pending,等待 await next() 返回 Promise,才会继续执行第一个中间件 await next() 后面的代码。
  3. 同样的在执行第二个中间件的时候,遇到 await next() 的时候,会手动执行第三个中间件,await next() 后面的代码依然被 pending,等待 await 下一个中间件的 Promise.resolve。只有在接收到第三个中间件的 resolve 后才会执行后面的代码,然后第二个中间会返回 Promise,被第一个中间件的 await 捕获,这时候才会执行第一个中间件的后续代码,然后再返回 Promise
  4. 以此类推,如果有多个中间件的时候,会依照上面的逻辑不断执行,先执行第一个中间件,在 await next() 出 pending,继续执行第二个中间件,继续在 await next() 出 pending,继续执行第三个中间,直到最后一个中间件执行完,然后返回 Promise,然后倒数第二个中间件才执行后续的代码并返回Promise,然后是倒数第三个中间件,接着一直以这种方式执行直到第一个中间件执行完,并返回 Promise,从而实现文章开头那张图的执行顺序。

通过上面的分析之后,如果你要写一个 koa2 的中间件,那么基本格式应该就长下面这样:

async function koaMiddleware(ctx, next){
    try{
        // do something
        await next()
        // do something
    }
    .catch(err){
        // handle err
    }    
}



链接: https://fly63.com/article/detial/1318

KOA2框架原理解析和实现

koa是一个基于node实现的一个新的web框架,它是由express框架的原班人马打造的。它的特点是优雅、简洁、表达力强、自由度高。它更express相比,它是一个更轻量的node框架

koa-easywechat_一个基于koa2的微信开发中间件

koa-easywechat注意:koa-easywechat中间件要写在最前面,也就是要第一个use,因为我在ctx上挂载了一个wechat对象,这个对象实现了大部分的微信接口,这样才能保证开发者在自己的写路由里,获取到ctx.wechat进行自己的业务开发

koajs--基于node.js的下一代web开发框架

Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。 通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。

node.js中 koa 框架的基本使用方法

安装 koa、简单使用、级联中间件的概念、获取get请求参数、获取post表单数据和文件上传、路由中间件 koa-router

基于Koa(nodejs框架)对json文件进行增删改查

想使用nodejs(koa)搭建一个完整的前后端,完成数据的增删改查,又不想使用数据库,那使用json文件吧。本文介绍了基于koa的json文件的增、删、改、查。

Koa日志中间件封装开发详解

对于一个服务器应用来说,日志的记录是必不可少的,我们需要使用其记录项目程序每天都做了什么,什么时候发生过错误,发生过什么错误等等,便于日后回顾、实时掌握服务器的运行状态,还原问题场景

Koa中间件

Koa 应用程序是一个包含一组中间件函数的对象,它是按照类似堆栈的方式组织和执行的。当一个中间件调用 next() 则该函数暂停并将控制传递给定义的下一个中间件。当在下游没有更多的中间件执行后,堆栈将展开并且每个中间件恢复执行其上游行为

Koa使用koa-multer上传文件(上传限制、错误处理)

上传文件在开发中是很常见的操作,今天我选择使用koa-multer中间件来实现这一功能,除了上传文件外,我还会对文件上传进行限制,以及发生上传错误时的处理。由于原来的 koa-multer 已经停止维护,我们要使用最新的 @koa/multer

从零实现TypeScript版Koa

这篇文章会讲些什么?如何从零开始完成一个涵盖Koa核心功能的Node.js类库,从代码层面解释Koa一些代码写法的原因:如中间件为什么必须调用next函数、ctx是怎么来的和一个请求是什么关系

读 koa2 源码后的一些思考与实践

Nodejs官方api支持的都是callback形式的异步编程模型。问题:callback嵌套问题,koa2 是由 Express原班人马打造的,是现在比较流行的基于Node.js平台的web开发框架,Koa 把 Express 中内置的 router、view 等功能都移除了

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!