资讯专栏INFORMATION COLUMN

对Koa-middleware实现机制的分析

MageekChiu / 2933人阅读

摘要:现在我们从实现一个简易的方法开始探索其中的机制。其中内部的可以将上一个的返回值传递给外部。一言以蔽之实现了递归调用的方法。当执行到的中间件没有时并且返回的为时逆序执行。

本文发布在github.com/ssssyoki,欢迎star,issues共同交流。

Koa是基于Node.js的下一代web开发框架,相比Express更轻,源码只有几百行。与传统的中间件不同,在Koa 1.x中采用了generator实现中间件,这需要开发者熟悉ES6中的generator,Promise相关知识。

在Koa官方文档示例代码中,采用yield next为跳转信号,然后会逆序执行中间件剩下的代码逻辑。这其中的逻辑非常有趣,本文将对其进行简要的分析。

Section A:

Koa的中间件跑在co模块下,而co可以将异步“变为”同步,从而实现用同步的方法写异步代码,避免了Node.js大量的回调嵌套。现在我们从实现一个简易的co方法开始探索其中的机制。

function co(generator){
  let g = generator();
  let next = function(data){
      let result = g.next(data);
      
      if(result.done){
          return ;
      };
      
      if(result.value instanceof Promise){
          result.value.then(function(d){
              next(d);
          },function(err){
              next(err);
          });
      }else{
          next();
      };
  };
  
  next();
};

首先需要了解generator相关知识,接下来我们分析这段代码:

首先定义一个参数为generator的co函数,当传入generator后(即app.use(function *(){...}))定义next方法实现对generator(可以理解为状态机)的状态遍历,由于每次遍历器指向新的yield,返回结构如{value:"Promise","done":"true/false"}的值,当done的值为false时遍历状态完毕并返回,若为true则继续遍历。其中内部的g.next(data)可以将上一个yield的返回值传递给外部。同时,若generator中含有多个yield且遍历未完成(即result.valuePromise对象 && result.done === false),resolve()所传递的数据可以在接下来then()方法中直接使用,即递归调用,直到result.done === true遍历结束并退出。

这里可能存在一个疑惑,在第一次调用next()方法时data为undefined,那是否会导致error产生呢?其实V8引擎在执行时,会自动忽略第一次调用next()时的参数,所以只有从第二次使用next()方法时参数才是有效的。

一言以蔽之,co实现了Promise递归调用generator的next方法。

*仓库内co-example.js添加了测试代码,上述实例测试可以运行成功。
Section B:

理解了co的运行原理后,再来理解middleware的机制就容易多了。

middleware实现了所谓“逆序”执行,其实就是每次调用use()方法时,将generator存入数组(记为s)中保存。在执行的时候先定义一个执行索引(记为index)和跳转标记(记为turn,也就是yield next中的next),再定义一个保存generator函数对象的数组(记为gs)。然后获取当前中间件generator,接着获取该generator的函数对象,将函数对象放在gs数组内保存,再执行generator的next()方法。
执行开始后,根据返回的value进行不同的处理,如果是标记turn(即执行到了yield next),说明该跳到下一个中间件了,此时令index++,然后从数组g中获取下一个中间件重复上一个中间件的执行流程。

当执行到的中间件没有yield时,并且返回的donetrue时,逆序执行。从此前用于保存generator函数对象的gs数组中取出上一个generator对象,然后执行generator的next()方法,直到全部结束。

我们打开Koa的application.js文件:

/**
 * Use the given middleware "fn".
 *
 * @param {GeneratorFunction} fn
 * @return {Application} self
 * @api public
 */

app.use = function(fn){
  if (!this.experimental) {
    // es7 async functions are not allowed,
    // so we have to make sure that "fn" is a generator function
    assert(fn && "GeneratorFunction" == fn.constructor.name, "app.use() requires a generator function");
  }
  debug("use %s", fn._name || fn.name || "-");
  this.middleware.push(fn);
  return this;
};

显而易见,app.use()方法就是将generator传入this.middleware数组中。其他部分的逻辑源码注释非常清晰,不再赘述。

我们再打开Koa-compose模块的index.js文件:

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

function compose(middleware){
  return function *(next){
    if (!next) next = noop();

    var i = middleware.length;

    while (i--) {
      next = middleware[i].call(this, next);
    }

    return yield *next;
  }
}

其中最关键的就是while语句。将之前app.use()传入并存储在middleware中的generator逆序取出并执行,将每个generator执行后的结果(即generator() === iterator)作为参数传入下一个(按数组的顺序则为前一个)generator中,在最后一个generator(数组第一个)执行后得出的next变量(即第一个generator的iterator),执行yield *next(即执行第一个generator的iterator)将全部generator像链表般串联起来。根据yield *的特性,yield *next将依次执行所有套用的next(类似递归),从而形成所谓“正序执行再逆序执行”的流程。

从co到compose,代码只有短短几十行,但组合在一起却非常精巧奇妙,值得细细品味。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/91368.html

相关文章

  • Dubbo 源码分析20 Dubbo服务提供者、服务消费者并发度控制机制

    摘要:代码根据服务提供者和服务调用方法名,获取。代码根据服务提供者配置的最大并发度,创建该服务该方法对应的信号量对象。总结是控制消费端对单个服务提供者单个服务允许调用的最大并发度。 本文将详细分析< dubbo:service executes=/>与< dubbo:reference actives = />的实现机制,深入探...

    不知名网友 评论0 收藏0

发表评论

0条评论

MageekChiu

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<