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

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

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


版权声明:

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

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

相关推荐
SameX7 分钟前
初识 HarmonyOS Next 的分布式管理:设备发现与认证
前端·harmonyos
M_emory_34 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
Ciito38 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
成都被卷死的程序员1 小时前
响应式网页设计--html
前端·html
mon_star°1 小时前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel
Zrf21913184552 小时前
前端笔试中oj算法题的解法模版
前端·readline·oj算法
文军的烹饪实验室3 小时前
ValueError: Circular reference detected
开发语言·前端·javascript
Martin -Tang4 小时前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发4 小时前
解锁微前端的优秀库
前端
王解5 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js