我把Koa 源码核心思想应用到了公司项目
本文你可以学到
- 理解
koa2
洋葱模型核心源码compose
函数实现 - 理解函数式编程衍生范式——面向切面编程
- 除了看源码,实战把洋葱模型思想应用到
SDK
项目中
Koa2 源码实现
我想很多小伙伴应该都知 Koa
有一个洋葱模型的概念,
通过它控制中间件内部内容的执行顺序。先来复习一下中间件结构。
function logger(ctx,next){}
类似这种的每个函数都是一个中间件。然后这些中间件会被存放到一个 middlewares
中间件数组中。
然后依赖的核心库是 koa-compose
,去完成整个 middlewares
数组中函数的执行,他不是普通的遍历依次执行过程,里面有一些特殊实现,重点关注下 源码中 dispatch
函数实现。
源码如下,对核心代码部分进行了注释讲解
// compose 函数参数是前面提到的 middlewares 中间件数组
function compose(middleware) {
// 参数校验:判断middleware是否为数组
if (!Array.isArray(middleware))
throw new TypeError("Middleware stack must be an array!");
// 数组内容校验:中间件数组中每一项必须是一个方法
for (const fn of middleware) {
if (typeof fn !== "function")
throw new TypeError("Middleware must be composed of functions!");
}
// 返回一个方法,这个方法就是compose的结果
// 外部可以通过调用这个方法来开中间件数组的遍历
// 参数形式和普通中间件一样,都是context和next
return function (context, next) {
return dispatch(0); // 开始中间件执行,从数组第一个开始
// 中间件执行函数
function dispatch(i) {
let fn = middleware[i]; // 取出需要执行的中间件
// 如果i等于数组长度,说明数组已经执行完了
if (i === middleware.length) {
fn = next; // fn等于外部传进来的next,结束执行
}
// 如果外部没有传结束执行的next,直接就resolve
if (!fn) {
return Promise.resolve();
}
// 执行中间件,注意传给中间件接收的参数应该是context和next
// 传给下一个中间件的next是函数,一定注意这里是使用的bind dispatch.bind(null, i + 1)
// 所以中间件里面调用 next 的时候其实调用的是dispatch(i + 1),也就是执行下一个中间件
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}
对这段代码的实现进行了详细的注释,再次强调一下代码的核心部分
return Promise.resolve(fn(context, dispatch.bind(null,i+1)))
fn
执行的第二个参数实际是中间件数组中的函数引用(使用了 bind
函数),在中间件内部调用 next
实际调用的是 dispatch(i+1)
,也就是下一个中间件。
洋葱模型思想在项目中的应用
需求描述
我们要提供一个 Node.js
的 SDK
,在这个 SDK
中我们提供了一系列功能,本文要讲的是其中一个小部分:请求函数中聚合中间件实现,SDK
使用者发起一个请求会调用 SDK
的 request
请求函数,这个函数我们应该怎么封装呢?
SDK.request
调用理论上会执行下面的一系列中间件函数。
如图所示,包括的功能有 危险字符过滤
,日志记录
,响应处理
等等(这里就不一一列举了),它的实现正需要一个洋葱模型的机制,上分支是洋葱进入时前期处理的函数,然后交给并等待其他中间件处理面,下分支是扒开洋葱后期处理的过程,
理论科普:洋葱模型也叫面向切面编程。AOP 为 Aspect Oriented Programming 的缩写,中文意思为:面向切面编程,它是函数式编程的一种衍生范式。面向切面编程的是在现有程序中,加入或减去一些功能(函数中间件)不影响原有的代码功能。比如我们的
request
需求中去除log
记录中间件。
分析与代码实现
- 首先我们定义一个存放中间件的
moduleList
- 定义
compose
函数 - 执行
dispatch(0)
以及在dispatch
函数内部调用fn.call(null,context,dispatch.bind(null,i+1))
const moduleList = [
require('./dangerQuery'),
require('./log'),
...// 省略一部分
require('./resHandler')
];
export const compliations = (ctx:Context,next:Next)=>{
const context = {ctx,next};
cosnt composeFn = compose(moduleList);
composeFn(context);
}
function compose = (list:Array<Function>)=>{
if(!Array.isArray(list)){
throw new TypeError("ModuleList must be an array!");
}
for(const fn of list){
if(typeof fn !== 'function'){
throw new TypeError("ModuleList must be composed of functions!");
}
}
return function(context:{ctx:Context,next:Next}){
const dispatch = async (i:number)=>{
if(list.length>i){
const fn = list[i];
await fn.call(null,context,dispatch.bind(null,i+1))
}
}
return dispatch(0)
}
}
中间件实现很多注意的点,本文只是想把洋葱模型部分思想理解,并应用起来,实际每个中间件内部要支持可插拔机制和开关机制的;并且
moduleList
中最后一个中间件函数,实际函数的第二个next
参数已经为空了,不要再次执行,如果SDK是基于egg,midway
等进行封装的,最后一个中间件内部应使用await context.next()
感悟
- 好东西就要用起来,除了这部分,自己项目中用到洋葱模型思想的还比较多的,并且开源项目中也很多,比如
Webpack
,Redux
。可以看看他们的使用有哪些巧妙之处。 - 我们在看源码的过程中。不要为了看源码而看源码,最好看懂后应用起来才会真的掌握
- 面试过程中如果写到了
koa
,洋葱模型肯定是一个必考项,如果能把原理说清楚,并举例将思想用到了自己项目中我觉得也是一个加分项。
参考文章
- https://juejin.cn/post/7078905984772489247
- https://github.com/koajs/koa