koa洋葱模型源码简析+极简复现——简单的递归

文章详细解析了Koa框架中的中间件机制,通过示例代码展示了中间件的定义和使用,解释了洋葱模型的概念,并分析了`compose`函数在其中的作用,以及中间件的执行顺序和递归过程。最后,提供了一个简化版的Koa洋葱模型实现。
摘要由CSDN通过智能技术生成

引言

koa里的中间件就是一个函数而已。比如:

app.use(async (ctx, next) => {
  console.log(3);
  ctx.body = 'hello world';
  await next();
  console.log(4);
});

洋葱模型:

const Koa = require('koa');

const app = new Koa();

app.use(async (ctx, next) => {
  console.log(1);
  await next();
  console.log(5);
});
app.use(async (ctx, next) => {
  console.log(2);
  await next();
  console.log(4);
});
app.use(async (ctx, next) => {
  console.log(3);
  ctx.body = 'hello world';
});

app.listen(3001);

执行node index.js,请求3001端口,输出是1~5。

作者:hans774882968以及hans774882968以及hans774882968

本文CSDN:https://blog.csdn.net/hans774882968/article/details/128843088

本文juejin:https://juejin.cn/post/7195249847044145208/

本文52pojie:https://www.52pojie.cn/thread-1740931-1-1.html

源码分析

use函数是定义中间件的入口,传送门

  use (fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
    debug('use %s', fn._name || fn.name || '-')
    this.middleware.push(fn)
    return this
  }

只是维护了一个middleware数组。

接下来看看调用listen的时候做什么。传送门

  listen (...args) {
    debug('listen')
    const server = http.createServer(this.callback())
    return server.listen(...args)
  }

调用了callback

  callback () {
    const fn = this.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
  }

那么需要看compose。我们找到koa-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
      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)
      }
    }
  }
}

非常之短。看来node后端和前端一样是🐔⌨️🍚……

经过简单的几步追踪可以看到,中间件那些函数的执行位置就在compose定义的dispatch被执行时。compose返回的function会传给handleRequest,那么我们再看看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)
  }

所以中间件是在请求处理的时候执行的,调用fnMiddleware就是调用了compose返回的function,也就调用了dispatch。但是中间件最终由dispatch函数调用,所以要搞清楚洋葱模型的实现,还是要看dispatch函数。

  1. dispatch(0)表示第一个中间件即将执行,fn(context, dispatch.bind(null, i + 1))这句话表示某个中间件正式执行。
  2. 某个中间件调用next就是调用了dispatch.bind(null, i + 1),这样就产生了递归,下一个中间件即将开始执行。最后一个中间件结束后就会回到上一个中间件的next结束之后的位置。
  3. fnMiddleware(ctx)执行完,所有中间件都已经执行完。而中间件执行完,请求已经执行完,之后handleRequest还会做一些对返回值的处理,respond函数传送门
  4. index记录的是执行过的最靠后的中间件的下标。而dispatch的参数i是当前中间件的下标。如果i <= index则表明next在某个函数(中间件)执行了2次。这个i参数的引入就是为了检测某个函数(中间件)执行2次next的非法情况。
  5. handleRequest调用fnMiddleware的情况来看,next参数是undefined(可在node_modules里自己加一个console.log验证),所以if (i === middleware.length) fn = next的作用,就是允许在最后一个中间件调用next函数。

koa洋葱模型简化版实现

下面的代码为了简便,假设在listen时立刻处理请求,不考虑异常处理等额外问题。扔浏览器控制台即可快速运行验证。

function compose(middleware) {
  return (ctx) => {
    function dispatch(i) {
      if (i <= index) throw new Error('next() called multiple times');
      index = i;
      if (i >= middleware.length) return;
      const fn = middleware[i];
      return fn(ctx, () => dispatch(i + 1));
    }
    let index = -1;
    return dispatch(0);
  };
}
const app = {
  middleware: [],
  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    return this.middleware.push(fn);
  },
  handleRequest(fnMiddleware) {
    console.log('handleRequest');
    const ctx = {};
    fnMiddleware(ctx);
  },
  listen() {
    const fnMiddleware = compose(this.middleware);
    this.handleRequest(fnMiddleware);
  },
};
app.use(async (ctx, next) => {
  console.log(1);
  await next();
  console.log(6, ctx.body);
});
app.use(async (ctx, next) => {
  console.log(2);
  await next();
  console.log(5, ctx.body);
  ctx.body += ' acmer';
});
app.use(async (ctx, next) => {
  console.log(3);
  ctx.body = 'hello world';
  await next();
  console.log(4);
});

app.listen(3000);

参考资料

  1. https://juejin.cn/post/6854573208348295182
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值