写这篇文章的初衷来源于跟一个搞 java 的老弟讨论静态服务的话题。
老弟: node 是如何实现一个静态服务的。
我:node 只读过 koa-static 的源码(其他中间件的做法应该也差不多)。
然后我巴拉巴拉...加上给他截图的源码。
重点来了...
老弟: await next() 这行代码什么意思,next 对象是什么,他怎么知道 next 下一个要执行的是什么?
一连串的问题,既然要刨根问底,那么我也只能整理思路从源码出发进行讲解,随后也就有了本篇文章,记录一下。
源码分析
废话环节结束,进入正题,直接上源码:
可以看到这段代码非常简短,也比较简单。这段源码是 koa-compose 这个中间件,处理所有通过 use 注册的中间件,接下来分析他做了什么。
图上可以看出他 return 出的是一个函数,也是核心代码,按照逐行。
- 第一行,这句话的意思是检查参数是否是数组,如果不是,抛错(保证传进来的必须是一个中间件数组)。
- 2,3,4 遍历检查该参数的元素是否是 function,若有,抛错。
剩下的是关键代码:
function (context, next) context 就是我们日常用的 context 对象,在 http 监听到相应路由的时候会传入(koa 里面有,这里不介绍了),next 等下解释,往下看。 这个函数 return 的是 dispatch 函数执行返回的对象, dispatch 后面会发现是一个递归函数,根据 i 动态执行,return 的值有三种情况:
- Promise.reject(err)
- Promise.resolve()
- Promise.resolve(fn())
dispatch
第一行: i <= index, 符合条件,抛错。这里的错误有两种情况:
- i 为 初始值 -1 时,小于或等于 -1 都属于下标越界。
- 当前中间件和上次或上次以前的中间件是同一值,表示已经调用过了(这里是组装,最后面会讲到),通常出现这种情况是因为注册了多次或传进来的中间件数组有重复。
第二行:index = i,更改 index,备下一次迭代做检查,是为上一行服务的。
第三行:取出索引为 i 的中间件,下面的 fn 均是。
第四行:检查是否是最后一个中间件的下一个元素,是,把 fn 替换 为 next。
第五行:实际上这行是检查第四行,如果没有传 next,说明没有要初始化的中间件了,返回 Promise.resolve()。
第六行:跳过。
第七行:精华在这里!品,你细品...
- context,把 context 传递到 fn 中。
- dispatch.bind:bind,返回的是一个懒加载函数,第一个是引用指向,第二个就是把 下一个索引传进去,作用实际上就是把下一个中间件传递。
ok,完整代码就分析完了,然后全部串起来,仔细想,这块代码最后干了啥!!! 如果一次一次执行,就会发现 最初的是 [fn1,fn2,fn3,fn4,fn5],最后变为...这里还是写伪代码比较直观
function finalFn(ctx) {
return Promise.resolve(f1(ctx,
Promise.resolve(f2(ctx,
Promise.resolve(f3(ctx,
Promise.resolve(f4(ctx,
Promise.resolve(f5(ctx,
Promise.resolve()
))
))
))
))
))
}
更简化来表示 f1(fn2(f3(f4(f5())))),到这里就很清楚了这个中间件做了什么事情了吧!
上面整理的结果,也就是大家常说的 洋葱模型 ,因为他真的很像洋葱,一层一层拨开(心里的BGM有点刹不住了…)
下面上两张图:
觉得很有意思的同时,咱们再想一下为什么要这么做呢?其实,这个中间件处理机制跟着常用设计模式的责任链模式极为相像,责任链模式就是采用引用传递的模式来执行下个操作,好处也是面向对象中老生常谈的解耦,还有一个好处是灵活性,允许动态的新增,修改或删除等操作。
示例
下面通过写一个中间件来体验一下。
const koa = require("koa");
const parseBody = require("koa-body");
const app = new koa();
app.use(parseBody());
const mergeParams = async (ctx, next) => {
ctx.params = {};
Object.assign(ctx.params, ctx.query, ctx.request.query);
await next();
}
app.use(mergeParams);
app.use(ctx => {
console.log(ctx.params);
ctx.body = "success";
});
app.listen(3000);
上边这段代码做的事情是给ctx对象创建一个params对象,将ctx.query和ctx.request.body 的参数都放到params里。
测试: node serve.js 在浏览器,访问 http://127.0.0.1:3000/user?id=1
结语
到这里本篇文章就结束了,篇幅挺小的代码,实现的功能却很强大,壮哉!追其源,观其质,乐哉!