【笔记】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" 之后的中间件多次执行。

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

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


版权声明:

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

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

相关推荐
AC-PEACE16 分钟前
Vue 中 MVVM、MVC 和 MVP 模式的区别
前端·vue.js·mvc
播播资源19 分钟前
ChatGPT付费创作系统V3.1.3独立版 WEB端+H5端+小程序端 (DeepSeek高级通道+推理输出格式)安装教程
前端·ai·chatgpt·ai作画·小程序·deepseek·deepseek-v3
zhrb1 小时前
打开Firefox自动打开hao360.hjttif.com标签解决方案
前端·firefox
安大桃子1 小时前
Cesium实现深色地图效果
前端·gis·cesium
程楠楠&M1 小时前
uni-app(位置1)
前端·javascript·uni-app·node.js
破z晓1 小时前
uniapp 整合openlayers 编辑图形文件并上传到服务器
前端·javascript·uni-app
码农君莫笑1 小时前
Linux系统上同时打印到物理打印机并生成PDF副本方法研究
linux·前端·chrome·打印·信管通
xlxxy_1 小时前
ABAP数据库表的增改查
开发语言·前端·数据库·sql·oracle·excel
m0_748234901 小时前
Spring Boot项目接收前端参数的11种方式
前端·spring boot·后端
祈澈菇凉1 小时前
除了Axios,如何用fetch处理403错误?
前端·javascript·vue.js