用过Koa的,肯定对 Middleware(中间件) 有所了解,那我们就用中间件从现象出发,理解洋葱模型
先自定义两个中间件:
logTime:打印时间戳
module.exports=function() { returnasyncfunction(ctx, next) { console.log("next前,打印时间戳:", newDate().getTime()) awaitnext() console.log("next后,打印时间戳:", newDate().getTime()) } }
logUrl:打印路由
module.exports=function() { returnasyncfunction(ctx, next) { console.log("next前,打印url:", ctx.url) awaitnext() console.log("next后,打印url:", ctx.url) } } 在 index.js 中use: constKoa=require('koa') constapp=newKoa() constlogTime=require('./middleware/logTime') constlogUrl=require('./middleware/logUrl') // logTime app.use(logTime()) // logUrl app.use(logUrl()) // response app.use(asyncctx=> { ctx.body='Hello World' }) app.listen(3000)
现在启动服务,然后我们随便访问一个路由,比如 /test,会发现这样一个现象:
两个现象:
- 中间件的执行了两次
- 执行顺序奇怪,以next函数为分界点:先use的中间件,next前的逻辑先执行,但next后的逻辑反而后执行
第一反应肯定就是:Koa为什么要这么设计?
正常不应该是中间件按顺序从开始到结束执行吗?
确实,如果说使用中间件的场景不存在前后依赖的情况,从头到尾按顺序链式调用完全没问题。但如果存在依赖的情况呢?如果只链式执行一次,怎么能保证前面的中间件能使用之后的中间件所添加的东西呢?
比如上面两个例子,我在 logUrl 的中间件里,对url进行了处理,加上了一个时间戳,然后我想在 logTime 的中间件里拿到这个时间戳并打印
如果只链式执行一次的话,显然无法实现
然后就是顺序的问题,为什么以next为分界线,先use的中间件 next 之后的逻辑反而后执行呢?
还是上面的例子,如果我们在logUrl 中间件里对url加上去的时间戳,是从数据库里取出来的,logTime 中间件肯定得等 logUrl 跑完了才能拿到对应时间戳。所以如果 logTime 之中的next完成的话,肯定是logUrl这个中间件已经跑完了
因此,上述现象1、2的原因就很清晰了
这样我们可以就确定中间件的执行流必定是如下的一个流程:
- 外层中间件进行前期处理(next 前的逻辑)
- 调用next,将控制流交给下个中间件,并await其完成,直到后面没有中间件或者没有next函数执行为止
- 完成后一层层回溯执行各个中间件的后期处理(next 后的逻辑)
这就是洋葱模型
再放出那两张很经典的图:
Koa中,外层中间件称为上游,内层中间件称为下游,现在我们再回头看官网的描述:
先用我们自己的方式理解之后,官方描述就不会太晦涩了
知道了原因,我们肯定得手动来实现一下洋葱模型,加深理解
实现之前肯定得先看一波源码:
来,一步步分析一波:
首先,先看看middleware在源码里是什么数据类型:
然后按流程看,肯定先进app的listen函数:https://blog.csdn.net/hyqhyqhyqq/article/details/null
创建服务的时候,传入了callback函数的返回值,去看看callback函数:
重点就是这里了,我们上面的分析说明想要实现洋葱模型,下面两点缺一不可:
- 要把上下文ctx对象和下一个中间件next传给当前的中间件
- 必须要等待下一个中间件执行完,再执行当前中间件的后续逻辑
而这就是compose函数所做的事情,来自于 koa-compose,这里先暂时不贴源码,有一说一很绕,强行看有点难受
所以,我们可以先按自己的思路来试试:
应该不需要解释吧,这样肯定会报错:
因为执行mw2的时候(也就是mw1里的next),并没有把ctx 和 mw3传给它
那么问题来了:我们怎么才能在调用mw1的next时,把ctx 和 mw2给这个next呢?
那我们肯定就需要对middleware数组里的每个元素重新包装一下了,用什么包装呢?
看个例子:
bind会将当时的参数保留下来,这正是我们所需要的,因此,加上一点小小的改动:
这个时候我们再跑一下代码:
这不就实现了吗?刚刚我留了一个坑就是没放 koa-compose 的源码,下面是源码:
红框的部分就是核心代码,大家可以自己看看,如果感觉很绕,可以对比我上面的例子先理解的,贴一下我简化版的代码:
constmiddleware= [] letmw1=asyncfunction (ctx, next) { console.log("next前,第一个中间件") awaitnext() console.log("next后,第一个中间件") } letmw2=asyncfunction (ctx, next) { console.log("next前,第二个中间件") awaitnext() console.log("next后,第二个中间件") } letmw3=asyncfunction (ctx, next) { console.log("第三个中间件,没有next了") } functionuse(mw) { middleware.push(mw) } use(mw1) use(mw2) use(mw3) letfn=function (ctx) { returndispatch(0) functiondispatch(i) { letcurrentMW=middleware[i] if(!currentMW) { return } returncurrentMW(ctx, dispatch.bind(null, i+1)) } } fn()
OK啦,这样就大工告成了
总结一下吧:一开始通过现象和场景反推,明白了什么时洋葱模型,以及为什么Koa需要使用洋葱模型,最后就是利用源码,简化实现了一下洋葱模型