koa洋葱结构解析

经常在使用 koa 的时候,通过 .use 的形式来注册各种中间件,例如下面一段代码

js 复制代码
app.use(async (ctx, next) => {
  console.log(1);
  await next();
  console.log(2);
});
app.use(async (ctx, next) => {
  console.log(3);
  await next();
  console.log(4);
});

这里会输出 1,3,4,2,下面就来翻看一下源码看看这个中间件实现的具体原理。

在看具体代码之前,先温习一下,使用 koa 的最小运行代码是什么样的

js 复制代码
const Koa = require("koa");
const app = new Koa();

// response
app.use((ctx) => {
  ctx.body = "Hello Koa";
});

app.listen(3000);

可以看到,最后通过 listen 方法来启动服务,那我们重点先看下 use 和 listen 做了什么事情。

use

js 复制代码
  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
  }

代码比较少,这里直接贴上去了,use 的主要作用就是给 middleware 添加相对应的 fn。

listen

js 复制代码
  listen (...args) {
    debug('listen')
    const server = http.createServer(this.callback())
    return server.listen(...args)
  }

这里 server 是 http 的库的方法,我们先不管,主要看一下 this.callback 做了什么事情。

js 复制代码
  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)
      if (!this.ctxStorage) {
        return this.handleRequest(ctx, fn)
      }
      return this.ctxStorage.run(ctx, async () => {
        return await this.handleRequest(ctx, fn)
      })
    }

    return handleRequest
  }

handleRequest 这个函数的实现如下

js 复制代码
  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)
  }

可以看到,最终是把 this.compose 返回的 fn 进行了调用,那么由此可以知道 this.compose 就是具体中间件调度的具体实现。

compose

this.compose 可以在 constructor 中看到,默认情况下就是 koa-compose,这个库也非常精简只有 50 行代码,下面会通过注释的形式来对源码进行一个说明。

js 复制代码
// 省略部分注释和部分代码

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!");
  }
  // 这里默认返回一个函数,handleRequest函数会调用这个返回的函数,并且传递 context
  return function (context, next) {
    let index = -1;
    // 默认情况下执行一次 dispatch ,dispatch因为是函数声明所以会提升到做作用域顶部
    return dispatch(0);
    function dispatch(i) {
      // 通常情况下不会遇到,但是如果执行两次就会抛出异常,例如第一次调用index为-1,i为0,第二次执行则变成index为0,i也为0则抛出错误
      if (i <= index)
        return Promise.reject(new Error("next() called multiple times"));
      index = i;
      let fn = middleware[i];
      // 这里从koa的实现可以看到,是没有传递next的,所以这行代码可以跳过
      if (i === middleware.length) fn = next;
      // 执行到最后一项的时候直接返回不再继续递归下去
      if (!fn) return Promise.resolve();
      try {
        // 这里实现很巧妙,利用了bind的原理,bind的第一个参数为this,之后的参数为函数的预设值,最后返回一个函数
        // 然后根据Promise.resolve的实现规范,如果传递的Promise.resolve是一个Promise要等待新的Promise执行完成之后决定状态
        // 这里推荐看下PromiseA+规范实现
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

ok,这里基本上就讲完了,还是对照最初的示例来看

js 复制代码
app.use(async (ctx, next) => {
  console.log(1);
  await next();
  console.log(2);
});
app.use(async (ctx, next) => {
  console.log(3);
  await next();
  console.log(4);
});

这里传递给 compose(fn1, fn2),之后 i 为 0,返回

js 复制代码
const fn = async (ctx, next) => {
  console.log(1);
  await next();
  console.log(2);
};
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

之后 i 为 1 的时候

js 复制代码
const fn = async (ctx, next) => {
  console.log(3);
  await next();
  console.log(4);
};
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

此时 i 为 2,发现数组取不到值了,执行

js 复制代码
if (!fn) return Promise.resolve();

回到最最后一步,还没有解释为什么会洋葱结构这样来执行代码

js 复制代码
app.use(async (ctx, next) => {
  console.log(1);
  await next();
  console.log(2);
});
app.use(async (ctx, next) => {
  console.log(3);
  await next();
  console.log(4);
});

这里我们知道先执行第一个函数

  1. 输出 console.log(1);
  2. 执行的过程中遇到 await next() 会执行 next,而从源码 dispatch.bind(null, i + 1) 可以知道下一项就是第二个函数
  3. 执行第二个函数
  4. 输出 console.log(3)
  5. 继续执行 await next() 从源码知道 i 为 2 的时候,返回的是 if (!fn) return Promise.resolve();
  6. 执行 conosle.log(4),返回 Promise 状态为已完成,结果为 undefined
  7. 执行 console.log(2)

上面可能有点绕,但是其实 koa-compose 利用了事件循环的机制,对于微任务每次执行都会放到微任务队列,等待主线程执行栈调用,而栈的特点就是先进后出,所以这也是为什么会输出 1,3,4,2 的原因了。

最后

如果文章有说的不对地方欢迎指出,最后本人正在找工作,有相关 hc 岗位欢迎滴滴。

相关推荐
deli00712 分钟前
1 个汉字算几个 token?实测中英文 prompt 与代码的编码差异
前端
无限聚核19 分钟前
抖音 certificate.prepare 之后怎么验券?和美团 msconsume 差在哪,无限聚核
前端·微信小程序
2603_9658966228 分钟前
Grid 网格布局|CSS 二维页面排版体系
前端·css
有味道的男人44 分钟前
搜好货平台关键词商品搜索接口:分页容错与商品数据轻量化提取方案
服务器·前端·javascript
lovingsoft1 小时前
Chrome Lighthouse 详解
前端·chrome
Qlly2 小时前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝2 小时前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦2 小时前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡2 小时前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI2 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战