Koa 框架介绍
Koa 是一个 web 框架,目的是成为 web 应用和 API 开发领域中的一个更小、更富有表现力、更健壮的基石。Koa 和 Express 一样,提供中间件机制,但本身并没有提供任何中间件,能够让开发者更加快速而愉快地编写服务端应用程序。
Koa 框架精髓之一 - 洋葱模型
Koa 封装了很多很棒的功能。笔者认为其中最具代表性的就是洋葱模型。洋葱模型可以让开发者快速地将函数进行组合,通过对功能简单的函数组合从而实现复杂的功能。
这里借用一下 eggjs 官网的洋葱模型图片:
在请求到达服务器后,服务器可以根据自身的业务需求,像剥洋葱一样,对请求一步步进行拆解,然后对信息进行一步步的组装,最后将组装好的响应返回给客户端。
洋葱模型的思想不仅可以用于服务器端,也可以用于各个场景下的复杂逻辑拆分。
洋葱模型的应用实例
上面说过,洋葱模型的设计思想不仅仅可以作用于服务器的中间件逻辑,也可以应用于方方面面的逻辑组装于设计。
假设我们有这样一个客户端的逻辑:
1、往页面插入一个 dom 节点;
2、等待 1 秒;
3、对 dom 节点注入 css 动画,开始播放;
4、不断检测 dom 节点的屏幕位置,当到达某个位置后,暂停动画;
5、等待 1 秒;
5、把 dom 节点从页面上移除;
如果我们不使用任何设计模式,我们会把上述的逻辑写成 1 个大函数和 5 个小函数,每个小函数做的事情分别对应上述步骤,大函数包含着小函数的调用顺序以及相关逻辑,表现方式如下:
async function startAnimation() {
creatDom();
await delay(1000);
startAnimation();
await detectDom();
pauseAnimation();
await delay(1000);
removeDom();
}
这其中有个问题是需要考虑的,createDom 和 removeDom,startAnimation 和 pauseAnimation,这两对函数,它们的逻辑其实是非常类似的,而且它们对应的操作对象应该是同一个,如果一个函数发生了变更,势必要修改另外一个函数,否则会引起不可预料的错误。
而这,往往就是不好的设计的开始,随着系统越来越复杂,代码的维护成本会越来越高。
那有没有更好的组织方式,可以使得这个逻辑更加抽象,更加容易调整和复用呢?这时候,我们可以考虑利用洋葱模型对逻辑进行统一的规范和组织。
利用洋葱模型,我们可以进行如下设计:
有 4 个小函数:
1、dom 操作函数 - 负责 dom 的插入以及移除;
2、延迟函数 - 负责延迟指定时间,然后回调(不要求精准,只用 setTimeout 即可);
3、动画处理函数 - 负责动画开启 和 暂停;
4、检测函数 - 根据特定规则检测 dom;
完成后的调用代码如下:
async function start() {
const startAmination = compose([
domCtrl,
delay(1000),
animationCtrl,
detectDom,
]);
const ctx = {};
await startAmination(ctx);
}
调用逻辑顺序如下:
domCtrl - start // create dom
delay - start // delay 1s
animationCtrl - start // start animation
detectDom - start, end // detecting dom status until matched
animationCtrl - end // pause animation
delay - end // delay 1s
domCtrl -end // remove dom
domCtrl 代码:
async function domCtrl(ctx = {}, next) {
const dom = document.createElement('div');
const mainPanel = document.querySelector('#main-panel');
// 创建 dom
ctx.dom = dom;
mainPanel.appendChild(dom);
// 往洋葱模型深处走
await next();
// 销毁 dom
mainPanel.removeChild(dom);
delete ctx.dom;
}
animationCtrl 代码:
async function animationCtrl(ctx = {}, next) {
const { dom } = ctx;
if(dom === undefined) {
return next();
}
const rawClass = dom.className || '';
// 开始动画
dom.className = `${rawClass} custom-animation`;
// 往洋葱模型深处走
await next();
// 暂停动画
dom.className = `${dom.className} animation-paused`;
}
新的代码组织方式对比旧代码,有个明显的好处就是,dom 的创建和删除统一整合到了 domCtrl 函数中,动画的播放和暂停,统一整合到了 animationCtrl 中,每个函数只需要关注好自己的职责即可。意味着以后无论是修改 dom 的处理流程,还是改变动画的处理过程,只需要修改一个函数即可。
可见,利用洋葱模型的思想,有助于我们在日常开发过程中对复杂逻辑进行抽象和拆解,同时提高系统的健壮性和可扩展性。
完整的例子可以查看:compose example
洋葱模型的实现关键 - compose
compose 源码
版本:4.1.0
function compose (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!')
}
return function (context, next) {
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 {
// 关键点 1
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
compose 源码异常的少,但它的功能非常赞
画图理解源码
如何直观地理解上面的源码是个关键,我们尝试用文字和图来表现里面的逻辑
关键点在于如何形成链式调用,所以一开始我们先忽略 context
接下来,我们可以把 compose 执行后返回的函数看成如下的代码:
dispatch.bind(null, 0);
这样就对应上源码中标注着 关键点 1 的地方了
接下来,结合上述的例子,我们尝试把 middleware 数组里面的函数具名化,这样 middleware 的内容就如下所示:
[
domCtrl,
delay,
animationCtrl,
detectDom,
]
当执行 dispatch.bind(null, 0)()
时,意味着执行 middleware 中的第一个函数,返回的内容为
Promise.resolve( domCtrl(context, dispatch.bind(null, 1)) );
我们可以看到,上面以 Promise 的方式执行 domCtrl 函数,同时在 domCtrl 函数的第二个参数中,注入了 dispatch.bind(null, 1)
可以想象,当 dispatch.bind(null, 1)
被 domCtrl 调用时,会返回
Promise.resolve( delay(context, dispatch.bind(null, 2)) );
到了这里,我们就可以得到规律,middleware 中的函数会按照上面的方式被依次执行(每个函数都要执行 next),从而实现链式调用的效果
用图来表示,就是下面的效果:
心得
1、洋葱模型可以帮助我们在特定场景优化设计,提高代码的健壮性;
2、使用洋葱模型时,中间件函数需要遵循特定的 入参和出参 规范;(笔者观点:一定程度上的规范是保证系统可维护性和健壮性的关键)
3、中间件函数使用 next 函数时,如果直接同步调用 next()
,会导致整个调用链提前退出,所以使用 next 函数时,有两个可选规范:
a、await next()
b、return next()
聪明的读者肯定能很快地理解上面两个规范对应着不同的设计场景;