koa 洋葱模型调用机制及实现
提起 koa,很多同学第一句话肯定就是“洋葱模型”。
这个调用模型非常地形象,对它理解了之后,也会觉得它的机制非常地精妙。
洋葱模型
我们来看这样的一个例子:
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx, next) => {
console.log('a-1')
await new Promise((resolve, reject) => {
setTimeout(() => {
console.log('---after 2000 ms---')
console.log('a-2')
resolve()
}, 2000)
})
const nextResult = await next()
console.log(nextResult)
})
app.use(async (ctx, next) => {
console.log('b-1')
await new Promise((resolve, reject) => {
setTimeout(() => {
console.log('---after 2000 ms---')
console.log('b-2')
resolve()
}, 2000)
})
const nextResult = await next()
console.log(nextResult)
return 'b-3'
})
app.use(async (ctx, next) => {
console.log('c-1')
await new Promise((resolve, reject) => {
setTimeout(() => {
console.log('---after 2000 ms---')
console.log('c-2')
resolve()
}, 2000)
})
return 'c-3'
})
app.listen(10000, () => {
console.log('listening...')
})
启动应用,访问 **http://localhost:10000**, 它在控制台打印的结果如下:
a-1
---after 2000 ms---
a-2
b-1
---after 2000 ms---
b-2
c-1
---after 2000 ms---
c-2
c-3
b-3
可以看到,类似于 express.js 的 next
,每次调用的时候,就会开始执行下一个中间件。随着 next
的调用而逐级进入。
然后当最深层的中间件 return
或执行完毕时,就会跳到上一层的 await next()
处,并将返回值带入。像这样逐层返回。
这样调用方式被描述为 洋葱模型:
洋葱模型的实现
koa 是一个非常精简的库,它其实就做了两件简单的事情:
一件是创建了两个对象:request
和 response
, 通过 getter
和 setter
简化了对原生的 req
和 res
的读写。所以我们在 koa 中应该操作的是它的 request
和 response
这两个对象。并把他们都挂载到 context
对象上,也就是中间件的第一个参数。
另一个就是实现了洋葱模型的调用逻辑。在这里,我们简化一下它的实现:
首先搭建基本的骨架,包括:
- 一个属性
middlewares
,用来存放中间件 use
方法,用来新增中间件listen
方法,开启监听_handleRequest
方法,请求到来时,创建一个 Context,然后调用_compose
方法来执行中间件。
我们忽略 Context
的具体实现,来看代码:
const http = require('http');
const Context = require('./context');
module.exports = class Koa {
constructor() {
this.middlewares = []
}
use(middleware) {
this.middlewares.push(middleware)
}
_compose(context) {
// ....
}
_handleRequest(req, res) {
const ctx = new Context(req, res)
this._compose(ctx).then(() => {
console.log('end')
}).catch((err) => {
console.log('err')
})
}
listen(...args) {
const server = http.createServer((req, res) => {
this._handleRequest(req, res)
})
server.listen(...args)
}
}
然后我们具体实现 _compose
方法:
compose(context) {
const dispatch = (index) => {
if (index === this.middlewares.length) {
return Promise.resolve()
}
const middleware = this.middlewares[index]
const next = () => dispatch(index + 1)
try {
return Promise.resolve(middleware(context, next))
} catch(err) {
return Promise.reject(err)
}
}
return dispatch(0)
}
好,带入文章开头的例子中进行执行,可以看到和原生的 koa
的结果一致。
现在我们就已经实现了洋葱模型了。当然 koa 源码中还有部分其他的情况的处理,但是大体逻辑就是这样。
是不是非常精妙?
后记
前几天我写了一篇 express 结构剖析,其中细致地分析了 express 复杂的结构。而 koa 却比它内容很多,它本身就是这样一个精简的架子,很多功能并没有像 express 内置进去了,而是调用额外的中间件库去实现这个功能。
express 和 koa 是现在很多功能更全的框架的底层框架,厘清他们的实现原理,有助于我们更好地学习和使用更大而全的框架,如 Nest.js 和 EGG.js。
转载自:https://juejin.cn/post/7128777268339408927