一. 什么是洋葱模型?
先看🌰
const Koa = require('koa');
const app = new Koa();
const PORT = 3000;
// #1
app.use(async (ctx, next)=>{
console.log(1)
await next(); //暂停当前程序
console.log(1)
});
// #2
app.use(async (ctx, next) => {
console.log(2)
await next();
console.log(2)
})
app.use(async (ctx, next) => {
console.log(3)
})
app.listen(PORT);
console.log(`http://localhost:${PORT}`);
// 输出
1
2
3
2
1
在 koa 中,app.use注册的中间件的回调按照洋葱模型迭代的。中间件被 next()方法分成了两部分。next()方法上面部分会先执行。next()方法下面的部分会在后面的中间件执行全部结束后执行。结合下图可以直观的看出:
洋葱内的每一层都表示一个独立的中间件,用于实现不同的功能,比如异常处理、缓存处理等。每次请求都会从左侧开始一层层地经过每层的中间件,当进入到最里层的中间件之后,就会从最里层的中间件开始逐层返回。因此对于每层的中间件来说,在一个 请求和响应 周期中,都有两个时机点来添加不同的处理逻辑。
中间件执行顺序:
洋葱模型可以干什么?
比如计算一次请求的耗时,且获取其他中间件的信息,如果没有洋葱模型是没有办法实现的。
二. koa原理
深入学习koa原理,通过查阅koa源码,我们看到有下面这些方法。
- use方法:维护得到 middleware 中间件数组
use (fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
debug('use %s', fn._name || fn.name || '-')
this.middleware.push(fn)
return this
}
- listen方法:Node.js 原生 http 模块 createServer 方法创建了一个服务。
- callback方法:服务的回调就是callback方法,返回是一个Promise函数。
listen (...args) {
debug('listen')
// 创建一个服务
const server = http.createServer(this.callback())
return server.listen(...args)
}
callback () {
// 返回一个函数
const fn = compose(this.middleware)
if (!this.listenerCount('error')) this.on('error', this.onerror)
const handleRequest = (req, res) => {
// 创建 ctx 上下文环境
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
// 执行 compose 函数中返回的 Promise 函数并返回结果。
return handleRequest
}
compose来自koa-compose模块,方法中的dispatch遍历整个middleware,然后将context和dispatch(i + 1)传给middleware中的方法。
主要目的【洋葱模型的核心】:
1). 将context一路传下去给中间件
2). 将middleware中的下一个中间件fn作为未来next的返回值
function compose(middleware) {
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next() {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
在看createContext方法和handleRequest方法,是把ctx和中间件进行绑定
handleRequest (ctx, fnMiddleware) {
const res = ctx.res
res.statusCode = 404
const onerror = err => ctx.onerror(err)
const handleResponse = () => respond(ctx)
onFinished(res, onerror)
return fnMiddleware(ctx).then(handleResponse).catch(onerror)
}
createContext (req, res) {
const context = Object.create(this.context)
const request = context.request = Object.create(this.request)
const response = context.response = Object.create(this.response)
context.app = request.app = response.app = this
context.req = request.req = response.req = req
context.res = request.res = response.res = res
request.ctx = response.ctx = context
request.response = response
response.request = request
context.originalUrl = request.originalUrl = req.url
context.state = {}
return context
}
三. 参考资料
- https://juejin.cn/post/7012031464237694983
- https://juejin.cn/post/7046713578547576863
- https://juejin.cn/post/6890259747866411022