koa中间件原理 && yield && generator

本文详细介绍了Koa框架中中间件的工作原理,包括如何利用Generator函数实现同步操作,以及通过co模块实现自动执行过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


  • 首先说下什么是中间件:

    中间件函数能够访问请求对象 (req)、响应对象 (res) 以及应用程序的请求/响应循环中的下一个中间件函数。

    换句话说,中间件类似过滤器,在请求和相应到来时,先进行处理掉一些相对简单的逻辑

  • 中间件过程的主要逻辑大致是:

    1. 使用use进行路由和handle函数的绑定,通过一个全局的数组(假定routes)记录每个use的匹配(路由->处理函数)
    2. 在请求到来的时候,对请求的路由进行匹配,在routes中遍历,只要和请求的路由匹配,都纳入处理函数的数组中(handle)
    3. 通过参数个数的不同,区分异常处理函数和普通处理函数,然后分成两组
    4. 遍历普通处理函数数组,执行中间件
    5. 如果中间件的执行过程中出现错误,把err传递给next函数,进行异常处理函数的调用
  • express框架中的中间件和以上提到的逻辑是一样的,略去不表。

    但是Koa框架中的中间件有些特殊,它使用了es6的新特性,把中间件的过程变得有些不一样:

    中间件中通过yield next进行下一个中间件的调用,等到调用链最顶端的那个函数处理完之后,就逆序返回,继续执行之前的中间件逻辑

  • es6中引入了Generator函数,和python中的Generator差不多,类似一个状态机,封装了多个内部状态。通过yield语句进行『暂停』,输出当前的状态。(但是有个限制就是yield语句只能在Generator函数中使用,不能在不同函数中使用。)因此通过yield可以实现同步的操作。

    异步编程中有『协程』的概念,A任务执行,中间把执行权交给B,自己挂起,B执行一段时间后,适时把执行权再返回给A继续执行,以此实现同步。

    因此在Generator函数中实现同步,就可以在请求资源的时候,函数A把执行权交出,通过yield 申请异步的资源,然后异步请求完成后,通过调用A.next(),把执行权返回给A,A就可以继续执行,这时候A已经拿到了异步申请的资源

  • 上述流程,如果有多次的yield,手动执行next肯定是很不对的。es6中有个co模块,通过递归调用next以实现函数的自动执行:
        function co(gen){
            var g = gen()

            function run(ret){
                if(ret.done) return
                if(ret.value instanceof Promise) ret.value.then((d)=>{

                    run(g.next(d))
                })  
                else run(g.next())
            }

            run(g.next())
        }

通过传入一个generator的函数,然后在run函数中进行递归,如果yield的是一个promise对象,那么下次第调用就要从他的处理函数中调用next方法。(并且由于generator函数是失忆的,不会保存上一次调用的结果,所以需要从then方法中获取promise传给处理函数的数据data,然后返回回去)。

注意这里不能把递归写成while……刚开始的时候自己想着写个while就好了…然而这样由于while每次只是定义了继续next,如果ret的value是promise,那么还只是定义了promise的then方法。又由于js的event loop机制,异步的函数会放在另一个事件池中,等到该次的事件执行结束了,再进行调用…于是while久等事件不至,进入了死循环…然后就发生了死锁……囧…… (啊……上次把页面跑崩还是在面试官的电脑上(真是不堪回首)

  • 有了co,再把中间件的概念加上。首先因为会有许多中间件,所以用全局变量gg数组保存中间件的每个处理函数。由于可能一个中间件在还没有处理完的时候调用了yield next,为了在next函数处理完之后可以返回到之前的状态,进行后续的处理,需要一个gs数组来保存那些执行到一半的函数的状态。 修改下代码:
var gg = []
function use(gen){
    gg.push(gen)
}
function trigger(){
    var ne = {},
    gs = [],
    index = 0

    function next(){
        var gen = gg[index](ne)
        gs.push(gen)
        co(gen)
    }

    function co(gen, data){
        if(!gen) return
        var result = gen.next(data)  //记得加data,不然白传了,data在下一次调用的时候使用
        if(result.done) {
            index--
            if(gs[index]) 
                co(gs[index])
            return
        }

        if(result.value instanceof Promise)
            result.value.then((d)=>{
                co(gen, d)
            })
        else if(result.value === ne) {
            index++
            next()
        } else 
            co(gen)
    }
    next()
}
  • 在koa中的源码是这样的:
app.use = function(fn){
  if (!this.experimental) {
    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;
};

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;
  }
}
co.wrap(compose(this.middleware)); //调用的时候。。然而实际上就是调用了co

function co(gen) {
  var ctx = this;
  var args = slice.call(arguments, 1)
  //保存执行状态
  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);

    onFulfilled();

    /**
     * @param {Mixed} res
     * @return {Promise}
     * @api private
     */

    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    /**
     * @param {Error} err
     * @return {Promise}
     * @api private
     */

    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    /**
     * Get the next value in the generator,
     * return a promise.
     *
     * @param {Object} ret
     * @return {Promise}
     * @api private
     */

    function next(ret) {
      if (ret.done) return resolve(ret.value);
      var value = toPromise.call(ctx, ret.value);
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'));
    }
  });
}

参考资料:

  1. http://www.cnblogs.com/Leo_wl/p/4684633.html
  2. https://segmentfault.com/a/1190000002738448
  3. http://es6.ruanyifeng.com/#docs/async
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值