【笔记】Koa 洋葱模型的学习与简单实现

Koa 洋葱模型

这是对于 Koa 洋葱模型实现的分析和学习。

Koa 的洋葱模型指的是以 next 函数为分割点,先由外到内执行 Request 的逻辑,再由内到外执行 Response 的逻辑。

就是说每一个中间件都是一个责任层。所以核心关键点是 next 函数。

官方例子

js 复制代码
app.use(async (ctx, next) => {
  const start = Date.now()

  await next()

  const ms = Date.now() - start
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
  ctx.body = 'Hello Koa'
})

中间件注册

由于是 "洋葱" ,那么 use 函数必然是可以多次调用定义的,那么就可以理解为需要一个数组来存储。

那么这里就可以定义一个数组叫 middlewares

当然叫中间件队列也可以,也算符合 Koa 自己的定义。因为它是需要按照定义顺序来决定执行顺序的,也就是说出入 "层" 的顺序。

js 复制代码
const middlewares = [] // 中间件调用队列

use 的实现就较为简单了,主要是为了向队列中填充定义的中间件。

按着官方例子,那么我们就简单的用对象来实现一下。

js 复制代码
const middlewares = [] // 中间件调用队列

const app = {
  use: fn => {
    middlewares.push(fn)
  },
}

// e.q.
app.use(() => {
  console.log(1)
})
app.use(() => {
  console.log(2)
})
app.use(() => {
  console.log(3)
})

// 调用队列
middlewares.forEach(fn => fn())
// 输出
// 1
// 2
// 3

中间件的调用

现在我们可以假定一个场景需要达成输出结果为: A1 -> B1 -> C1 -> C2 -> B2 -> A2

js 复制代码
// 假定注册了 3 个中间件,需要输出结果为:A1 -> B1 -> C1 -> C2 -> B2 -> A2
app.use(async next => {
  console.log('A', 1)
  await next()
  console.log('A', 2)
})
app.use(async next => {
  console.log('B', 1)
  await next()
  console.log('B', 2)
})
app.use(async next => {
  console.log('C', 1)
  await next()
  console.log('C', 2)
})

看到此例子,你能想到的能达到这样输出结果的实现逻辑是什么?

尝试一下

js 复制代码
function fn(i) {
  if (i > 2) return

  console.log(i)

  fn(i + 1)

  console.log(i)
}

fn(0)

没错,那就是递归啦,这里的递归会将输出:0 1 2 2 1 0

换句话说,那就是 递归点 的调用就成为了 分割点,将这个函数的执行分为了上下两部分。

基于递归的实现

那么这里我们就可以结合上面定义实现出第一版中间件的执行逻辑。

因为队列里注册的是可执行函数,那么就要考虑如何执行函数的情况。

js 复制代码
function dispatch(i) {
  // 当调用中间件到队尾的时候需要处理
  if (i > middlewares.length) return

  // 取出当前需要执行的中间件
  const currentFn = middlewares[i]

  // 递归点的进一步实现,即是将本身作为中间件函数的参数
  // 就是成为了 next 函数
  currentFn(() => dispatch(i + 1))

  // 换一种理解
  // const next = () => dispatch(i + 1)
  // currentFn(next)
}

dispatch(0)

// 输出
// A 1
//   B 1
//     C 1
//     C 2
//   B 2
// A 2

此时一个最基本的 "洋葱" 模型的实现逻辑就完成,但这还是没有考虑 async/awaitPromise 的情况。即是异步执行会出现问题。

考虑异步情况

此时的中间件例子假设为

js 复制代码
app.use(async next => {
  console.log('A', 1)
  await next()
  console.log('A', 2)
})
app.use(async next => {
  console.log('B', 1)

  console.time('B')
  await new Promise(resolve => {
    setTimeout(() => {
      console.timeEnd('B')
      resolve()
    }, 2000)
  })

  await next()
  console.log('B', 2)
})
app.use(async next => {
  console.log('C', 1)
  await next()
  console.log('C', 2)
})

那么就需要考虑将执行的过程包装成 Promise 实例,亦可对 async/await 生效。

js 复制代码
function dispatch(i) {
  // 当调用中间件到队尾的时候需要处理
  // 即是成功执行到队尾了,所以使用 resolve
  if (i > middlewares.length) return Promise.resolve()

  // 取出当前需要执行的中间件
  const currentFn = middlewares[i]
  // 如果取空的情况也可以认为到队尾了,所以使用 resolve
  if (!currentFn) return Promise.resolve()

  try {
    // 将执行过程包装成 Promise 实例
    return Promise.resolve(currentFn(() => dispatch(i + 1)))
  } catch (error) {
    // 如果出错则需要 reject 来中断继续执行中间件队列
    return Promise.reject(error)
  }
}

dispatch(0)

// 输出
// A 1
//   B 1
//   B: 2000+ms
//     C 1
//     C 2
//   B 2
// A 2

那么现在就已经完成了一个能够支持 Promiseasync/await"洋葱" 模型的大致逻辑了。

当然这里只是 大致的逻辑 ,中间还有很多细节是没有实现的,这里只是 分析和学习,关于哪些细节可以自己去尝试实现或者按自己的理解进行完善。

到了这,不知道是否会发现还有一个问题没有解决?就是如果一个中间件里的 next 函数被调用了多次会导致队列会被执行多次

例子假设为

js 复制代码
app.use(async next => {
  console.log('A', 1)
  await next()
  await next()
  console.log('A', 2)
})
app.use(async next => {
  console.log('B', 1)

  console.time('B')
  await new Promise(resolve => {
    setTimeout(() => {
      console.timeEnd('B')
      resolve()
    }, 2000)
  })

  await next()
  console.log('B', 2)
})
app.use(async next => {
  console.log('C', 1)
  await next()
  console.log('C', 2)
})

留个问题 1

这时如果按之前的实现那么就会出现 "A1" 之后的中间件多次执行。

那么应该如何解决这个问题呢?

你能想到什么方法来解决?


版权声明:

本文版权属于作者 林小帅,未经授权不得转载及二次修改。

转载或合作请在下方留言及联系方式。

相关推荐
梦帮科技5 小时前
GRAVIS v4.0:基于Web的极速套利架构设计与实时数据流实现
前端·人工智能·rust·自动化·区块链·智能合约·数字货币
爱勇宝5 小时前
办公资料反复修改、补传、交接混乱,我做了个桌面工具来解决这件事
前端·后端·程序员
微信开发api-视频号协议6 小时前
企业微信外部群开发自动化实践过程
java·前端·微信·自动化·企业微信·ipad
甲维斯6 小时前
Fable5:20美金的顶级设计师!
前端·人工智能
kyriewen6 小时前
我同时用了 Claude Code、Cursor 和 Codex,说说三个工具的真实差距——2026 年选错工具比不用 AI 更浪费时间
前端·ai编程·claude
Jackson__6 小时前
为了拯救我的腰椎和颈椎,我做了款浏览器插件!
前端·javascript·开源
闲猫7 小时前
Python FastAPI + SQLAlchemy 入门教程:从零搭建你的第一个 Web 应用
前端·python·fastapi
林小帅7 小时前
NestJS v11 + Prisma v7 ESM 迁移配置解析
前端·javascript·后端
IT_陈寒7 小时前
React的setState竟然不是立刻生效的,害我调试半天
前端·人工智能·后端
简简单单就是我_hehe7 小时前
前端埋点日志解决方案:完整 User-Agent 解析工具实现,爬虫识别、设备品牌、操作系统、浏览器版本全解析(附完整源码)
前端·爬虫