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/await 、Promise 的情况。即是异步执行会出现问题。
考虑异步情况
此时的中间件例子假设为
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
那么现在就已经完成了一个能够支持 Promise 、async/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" 之后的中间件多次执行。
那么应该如何解决这个问题呢?
你能想到什么方法来解决?
版权声明:
本文版权属于作者 林小帅,未经授权不得转载及二次修改。
转载或合作请在下方留言及联系方式。