KOA2 compose 串联中间件实现(洋葱模型)

AI权益加码!Claude Code、Cursor等20+工具免费用! 购周边限时加赠Coding Plan Lite,畅享主流AI工具!学习进阶更高效! 阅读详情

Koa 是当下主流 NodeJS 框架,以轻量见长,而它中间件机制与相对传统的 Express 支持了异步,所以编码时经常使用 async/await ,提高了可读性,使代码变得更优雅,上一篇文章 NodeJS 进阶 —— Koa 源码分析 ,也对 “洋葱模型” 和实现它的 compose 进行分析,由于个人觉得 compose 的编程思想比较重要,应用广泛,所以本篇借着 “洋葱模型” 的话题,打算用四种方式来实现 compose 。

洋葱模型案例

如果你已经使用 Koa 对 “洋葱模型” 这个词一定不陌生,它就是 Koa 中间件的一种串行机制,并且是支持异步的,下面是一个表达 “洋葱模型” 的经典案例。

const Koa = require("koa");
const app = new Koa();
app.use(asycn (ctx, next) => {
 console.log(1);
 await next();
 console.log(2);
});
app.use(asycn (ctx, next) => {
 console.log(3);
 await next();
 console.log(4);
});
app.use(asycn (ctx, next) => {
 console.log(5);
 await next();
 console.log(6);
});
app.listen(3000);

上面的写法我们按照官方推荐,使用了 async/await ,但如果是同步代码不使用也没有关系,这里简单的分析一下执行机制,第一个中间件函数中如果执行了 next ,则下一个中间件会被执行,依次类推,就有了我们上面的结果,而在 Koa 源码中,这一功能是靠一个 compose 方法实现的,我们本文四种实现 compose 的方式中实现同步和异步,并附带对应的案例来验证。

准备工作

在真正创建 compose 方法之前应该先做些准备工作,比如创建一个 app 对象来顶替 Koa 创建出的实例对象,并添加 use 方法和管理中间件的数组 middlewares 。

文件:app.js

// 模拟 Koa 创建的实例

const app = {
 middlewares: []
};
// 创建 use 方法
app.use = function(fn) {
 app.middlewares.push(fn);
};
// app.compose.....
module.exports = app;

上面的模块中导出了 app 对象,并创建了存储中间件函数的 middlewares 和添加中间件的 use 方法,因为无论用哪种方式实现 compose 这些都是需要的,只是 compose 逻辑的不同,所以后面的代码块中会只写 compose 方法。

Koa 中 compose 的实现方式

首先介绍的是 Koa 源码中的实现方式,在 Koa 源码中其实是通过 koa-compose 中间件来实现的,我们在这里将这个模块的核心逻辑抽取出来,用我们自己的方式实现,由于重点在于分析 compose 的原理,所以 ctx 参数就被去掉了,因为我们不会使用它,重点是 next 参数。

1、同步的实现

文件:app.js

app.compose = function() {
 // 递归函数
 function dispatch(index) {
 // 如果所有中间件都执行完跳出
 if (index === app.middlewares.length) return;
 // 取出第 index 个中间件并执行
 const route = app.middlewares[index];
 return route(() => dispatch(index + 1));
 }
 // 取出第一个中间件函数执行
 dispatch(0);

上面是同步的实现,通过递归函数 dispatch 的执行取出了数组中的第一个中间件函数并执行,在执行时传入了一个函数,并递归执行了 dispatch ,传入的参数 +1 ,这样就执行了下一个中间件函数,依次类推,直到所有中间件都执行完毕,不满足中间件执行条件时,会跳出,这样就按照上面案例中 1 3 5 6 4 2 的情况执行,测试例子如下(同步上、异步下)。

文件:sync-test.js

const app = require("./app");
app.use(next => {
 console.log(1);
 next();
 console.log(2);
});
app.use(next => {
 console.log(3);
 next();
 console.log(4);
});
app.use(next => {
 console.log(5);
 next();
 console.log(6);
});
app.compose();

文件:async-test.js

const app = require("./app");
// 异步函数
function fn() {
 return new Promise((resolve, reject) => {
 setTimeout(() => {
 resolve();
 console.log("hello");
 }, 3000);
 });
}
app.use(async next => {
 console.log(1);
 await next();
 console.log(2);
});
app.use(async next => {
 console.log(3);
 await fn(); // 调用异步函数
 await next();
 console.log(4);
});
app.use(async next => {
 console.log(5);
 await next();
 console.log(6);
});
app.compose();

我们发现如果案例中按照 Koa 的推荐写法,即使用 async 函数,都会通过,但是在给 use 传参时可能会传入普通函数或 async 函数,我们要将所有中间件的返回值都包装成 Promise 来兼容两种情况,其实在 Koa 中 compose 最后返回的也是 Promise,是为了后续的逻辑的编写,但是现在并不支持,下面来解决这两个问题。

注意:后面 compose 的其他实现方式中,都是使用 sync-test.js 和 async-test.js 验证,所以后面就不再重复了。

2、升级为支持异步

文件:app.js

app.compose = function() {
 // 递归函数
 function dispatch(index) {
 // 如果所有中间件都执行完跳出,并返回一个 Promise
 if (index === app.middlewares.length) return Promise.resolve();
 // 取出第 index 个中间件并执行
 const route = app.middlewares[index];
 // 执行后返回成功态的 Promise
 return Promise.resolve(route(() => dispatch(index + 1)));
 }
 // 取出第一个中间件函数执行
 dispatch(0);

我们知道 async 函数中 await 后面执行的异步代码要实现等待,带异步执行后继续向下执行,需要等待 Promise,所以我们将每一个中间件函数在调用时最后都返回了一个成功态的 Promise,使用 async-test.js 进行测试,发现结果为 1 3 hello(3s后) 5 6 4 2 。

Redux 旧版本 compose 的实现方式

1、同步的实现

文件:app.js

app.compose = function() {
 return app.middlewares.reduceRight((a, b) => () => b(a), () => {})();

上面的代码看起来不太好理解,我们不妨根据案例把这段代码拆解开,假设 middlewares 中存储的三个中间件函数分别为 fn1 、 fn2 和 fn3 ,由于使用的是 reduceRight 方法,所以是逆序归并,第一次 a 代表初始值(空函数), b 代表 fn3 ,而执行 fn3 返回了一个函数,这个函数再作为下一次归并的 a ,而 fn2 作为 b ,依次类推,过程如下。

// 第 1 次 reduceRight 的返回值,下一次将作为 a
() => fn3(() => {});
// 第 2 次 reduceRight 的返回值,下一次将作为 a
() => fn2(() => fn3(() => {}));
// 第 3 次 reduceRight 的返回值,下一次将作为 a
() => fn1(() => fn2(() => fn3(() => {})));
由上面的拆解过程可以看出,如果我们调用了这个函数会先执行 fn1 ,如果调用 next 则会执行 fn2 ,如果同样调用 next 则会执行 fn3 , fn3 已经是最后一个中间件函数了,再次调 next 会执行我们最初传入的空函数,这也是为什么要将 reduceRight 的初始值设置成一个空函数,就是防止最后一个中间件调用 next 而报错。

经过测试上面的代码不会出现顺序错乱的情况,但是在 compose 执行后,我们希望进行一些后续的操作,所以希望返回的是 Promise,而我们又希望传入给 use 的中间件函数既可以是普通函数,又可以是 async 函数,这就要我们的 compose 完全支持异步。

2、升级为支持异步

文件:app.js

app.compose = function() {
 return Promise.resolve(
 app.middlewares.reduceRight(
 (a, b) => () => Promise.resolve(b(a)),
 () => Promise.resolve();
 )()
 );

参考同步的分析过程,由于最后一个中间件执行后执行的空函数内一定没有任何逻辑,但为遇到异步代码可以继续执行(比如执行 next 后又调用了 then ),都处理成了 Promise,保证了reduceRight 每一次归并的时候返回的函数内都返回了一个 Promise,这样就完全兼容了 async和普通函数,当所有中间件执行完毕,也返回了一个 Promise,这样 compose 就可以调用 then方法执行后续逻辑。

Redux 新版本 compose 的实现方式

1、同步的实现

文件:app.js

app.compose = function() {
 return app.middlewares.reduce((a, b) => arg => a(() => b(arg)))(() => {});

Redux 新版本中将 compose 的逻辑做了些改动,将原本的 reduceRight 换成 reduce ,也就是说将逆序归并改为了正序,我们不一定和 Redux 源码完全相同,是根据相同的思路来实现串行中间件的需求。

个人觉得改成正序归并后更难理解,所以还是将上面代码结合案例进行拆分,中间件依然是 fn1 、 fn2 和 fn3 ,由于 reduce 并没有传入初始值,所以此时 a 为 fn1 , b 为 fn2 。

// 第 1 次 reduce 的返回值,下一次将作为 a
arg => fn1(() => fn2(arg));
// 第 2 次 reduce 的返回值,下一次将作为 a
arg => (arg => fn1(() => fn2(arg)))(() => fn3(arg));
// 等价于…
arg => fn1(() => fn2(() => fn3(arg)));
// 执行最后返回的函数连接中间件,返回值等价于…
fn1(() => fn2(() => fn3(() => {})))
所以在调用 reduce 最后返回的函数时,传入了一个空函数作为参数,其实这个参数最后传递给了 fn3 ,也就是第三个中间件,这样保证了在最后一个中间件调用 next 时不会报错。

2、升级为支持异步

下面有个更艰巨的任务,就是将上面的代码更改为支持异步,实现如下。

文件:app.js

app.compose = function() {
 return Promise.resolve(
 app.middlewares.reduce((a, b) => arg =>
 Promise.resolve(a(() => b(arg)))
 )(() => Promise.resolve())
 );
};

实现异步其实与逆序归并是一个套路,就是让每一个中间件函数的返回值都是 Promise,并让compose 也返回 Promise。

使用 async 函数实现

这个版本是我在之前在学习 Koa 源码时偶然在一位大佬的一篇分析 Koa 原理的文章中看到的(翻了半天实在没找到链接),在这里也拿出来和大家分享一下,由于是利用 async 函数实现的,所以默认就是支持异步的,因为 async 函数会返回一个 Promise。

文件:app.js

app.compose = function() {
// 自执行 async 函数返回 Promise
return (async function () {
// 定义默认的 next,最后一个中间件内执行的 next
let next = async () => Promise.resolve();
// middleware 为每一个中间件函数,oldNext 为每个中间件函数中的 next
// 函数返回一个 async 作为新的 next,async 执行返回 Promise,解决异步问题

 function createNext(middleware, oldNext) {
 return async () => {
 await middleware(oldNext);
 }
 }

// 反向遍历中间件数组,先把 next 传给最后一个中间件函数
// 将新的中间件函数存入 next 变量
// 调用下一个中间件函数,将新生成的 next 传入

 for (let i = app.middlewares.length - 1; i >= 0; i--) {
 next = createNext(app.middlewares[i], next);
 }
 await next();
 })();
}

上面代码中的 next 是一个只返回成功态 Promise 的函数,可以理解为其他实现方式中最后一个中间件调用的 next ,而数组 middlewares 刚好是反向遍历的,取到的第一个值就是最后一个中间件,而调用 createNext 作用是返回一个新的可以执行数组中最后一个中间件的 async 函数,并传入了初始的 next ,这个返回的 async 函数作为新的 next ,再取到倒数第二个中间件,调用 createNext ,又返回了一个 async 函数,函数内依然是倒数第二个中间件的执行,传入的 next 就是上次新生成的 next ,这样依次类推到第一个中间件。

因此执行第一个中间件返回的 next 则会执行传入的上一个生成的 next 函数,就会执行第二个中间件,就会执行第二个中间件中的 next ,就这样直到执行完最初定义的的 next ,通过案例的验证,执行结果与洋葱模型完全相同。

至于异步的问题,每次执行的 next 都是 async 函数,执行后返回的都是 Promise,而最外层的自执行 async 函数返回的也是 Promise,也就是说 compose 最后返回的是 Promise,因此完全支持异步。

这个方式之所放在最后,是因为个人觉得不好理解,我是按照自己对这几种方式理解的难易程度由上至下排序的。

总结

或许你看完这几种方式会觉得,还是 Koa 对于 compose 的实现方式最容易理解,你也可能和我一样在感慨 Redux 的两种实现方式和 async 函数实现方式是如此的巧妙,恰恰 JavaScript 在被别人诟病 “弱类型”、“不严谨” 的同时,就是如此的具有灵活性和创造性,我们无法判断这是优点还是缺点(仁者见仁,智者见智),但有一点是肯定的,学习 JavaScript 不要被强类型语言的 “墨守成规” 所束缚(个人观点,强类型语言开发者勿喷),就是要吸收这样巧妙的编程思想,写出 compose 这种优雅又高逼格的代码,路漫漫其修远兮,愿你在技术的路上 “一去不复返”

Koa2 洋葱模型 —— compose 串联中间件的四种实现 原文出自:https://www.pandashen.com 前言 Koa 是当下主流 NodeJS 框架,以轻量见长,而它中间件机制与相对传统的 Express 支持了异步,所以编码时经常使用 async/await,提高了可读性,使代码变得更优雅,上一篇文章 NodeJS 进阶 —— Koa 源码分析,也对 “洋葱模型” 和实现它的 compose 进行分析,由于个人觉得 compose... 阅读详情

相关推荐

koa - 洋葱模型浅析

,它的核心工作包括下面两个方面:如下图: 如下是洋葱代码的案例: 4.源码解析 use方法 listen方法 compose方法 compose方法是洋葱模型的核心,compose方法中有一个dispatch方法,第一次调用的时候,执行的是第一个中间件函数,中间件函数执行的时候就是再次调用dispatch函数,也就说形成了一个递归,这就是next函数执行的时候会执行下一个中间件的原因,因此形成了一个洋葱模型。 5.为什么需要洋葱模型? 因为很多时候,在一个app里面有很多中间件,,用葱模型

weixin_38245947的博客 1217

koakoa-compose

介绍 koa-composekoa用到的类库,用于koa中间件合并执行,compose是将多个函数合并成一个函数(eg: a() + b() +c()=> c(b(c()))),主要使用Promise/Async、递归调用实现。整个代码包括注释不到50行,非常精巧。 安装 npm install koa-compose 用法 compose([fun a{}, fun b{}, fun ...

KK下山去买菜 1498

Koa2洋葱模型是什么?它是如何实现的?

在下一个中间件函数执行完毕后,控制权会再次回到当前中间件函数,执行剩余的代码。关键字的配合,实现中间件函数的顺序执行和控制权的传递。Koa2洋葱模型是一种中间件执行流程的设计模式,它允许开发者在请求的处理过程中,按照特定的顺序依次执行一系列中间件函数。然后,我们定义一些中间件函数,这些函数将按照洋葱模型的顺序依次执行。可以看到,中间件函数的执行顺序是从外到内,然后再从内到外。总结一下,Koa2洋葱模型是一种中间件执行流程的设计模式,通过。在每个中间件函数中,我们首先输出一个开始的日志,然后调用。

jakelihua 531

推荐一款高效能的Koa中间件组合工具:Koa-Compose

在Node.js的世界中,Koa是备受推崇的Web应用程序框架,它以其优雅的设计和强大的中间件机制著称。今天我们要向您推荐一个能够提升Koa应用开发效率的重要组件——`koa-compose`。 ## 1. 项目介绍 `koa-compose`是一个专门针对Koa框架设计的中间件编排库。它的核心功能是将多个中间件函数合并为一个,使得您可以更加流畅地管理应用程序的流程控制。通过简单的API,这个...

gitblog_00062的博客 653

Node.js 网络框架koa compose中间件使用分析

next 函数是一个回调函数,用于在当前中间件函数完成工作后调用下一个中间件函数。当内层的中间件函数完成工作并返回响应时,外层的中间件函数可以再次执行一些操作,然后将响应返回给客户端。compose() 函数的参数是一个中间件数组,它包含了要组合的中间件函数。在 Koa 中,中间件函数是按照特定顺序调用的函数,用于处理传入的 HTTP 请求并生成响应。如果当前中间件函数是最后一个中间件函数,就会将 next 函数赋值给当前中间件函数。如果没有更多的中间件函数,就会返回一个已完成的 Promise 对象。

Free雅轩的博客 373

Web应用开发框架-koa()——koa中间件之概念、洋葱模型-执行顺序、异步中间件koa-compose中间件合成-compsoe函数

compose的参数是函数,返回的也是一个函数因为除了第一个函数的接受参数,其他函数的接受参数都是上一个函数的返回值,所以初始函数的参数是多元的,而其他函数的接受值是一元的compsoe函数可以接受任意的参数,所有的参数都是函数,且执行方向是自右向左的,初始函数一定放到参数的最右面参考链接: https://segmentfault.com/a/1190000008394749。

weixin_44867717的博客 546

KOA2 compose 串联中间件实现洋葱模型

前言Koa 是当下主流 NodeJS 框架,以轻量见长,而它中间件机制与相对传统的 Express 支持了异步,所以编码时经常使用 async/await,提高了可读性,使代码变得更优雅,上一篇文章 NodeJS 进阶 —— Koa 源码分析,也对 “洋葱模型” 和实现它的 compose 进行分析,由于个人觉得 compose 的编程思想比较重要,应用广泛,所以本篇借着 “洋葱模型” 的话题,打...

weixin_34018202的博客 424

仿koa2 compose实现自己的中间件

仿koa2 compose实现自己的中间件 临时接到任务,要写中间件,然后就仿照了两篇博客进行了改动,这两篇博客会在最后给出。 app.js class App { constructor() { this.middlewares = []; } use(fn) { if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!'); this.mi

qq_32862143的博客 218

解密Koa.js洋葱模型中间件执行流程的核心原理

Koa.js作为一款轻量级的Node.js Web框架,其独特的**洋葱模型**中间件机制是实现高效请求处理的核心。这种设计模式允许开发者通过层层嵌套的中间件对HTTP请求进行精细化的拦截、处理和响应,极大提升了代码的可扩展性和复用性。本文将深入解析洋葱模型的工作原理,帮助新手开发者快速掌握这一核心概念。 ## 一、洋葱模型的核心概念:什么是中间件? 在Koa.js中,**中间件**是一个接收

gitblog_01086的博客 712

学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

面试题千万不要死记,一定要自己理解,用自己的方式表达出来,在这里预祝各位成功拿下自己心仪的offer。需要完整面试题的朋友可以点击蓝色字体获取f (!/***/自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

2301_76379381的博客 767

【源码学习----koakoa中间件核心(koa-compose)源码解读分析(洋葱模型解析)

最近经常使用koa进行服务端开发,迷恋上了koa洋葱模型,觉得这玩意太好用了。而且koa是以精简为主,没有很多集成东西,所有的东西都需按需加载,这个更是太合我胃口了哈哈哈哈。 相对与express的中间件,express的中间件使用的是串联,就像冰糖葫芦一样一个接着一个,而koa使用的V型结构(洋葱模型),这将给我们的中间件提供更加灵活的处理方式。 基于对洋葱模型的热衷,所以对koa洋葱模型进行一探究竟,不管是koa1还是koa2中间件都是基于koa-compose进行编写的,这种V型结构的实现就来源

零信号的博客 385

浅析Node中间件Koa&Express:原理和实现

说到中间件,很多开发者都会想到 Koa.js,其中间件设计无疑是前端中间件思想的典型代表之一。 最近重新温习这部分内容,按奈不住想要和各位看官聊聊其中绝妙! Koa用起来非常方便——比之express,它“完美中间件”的设计让功能之间看起来非常简洁!笔者在项目中就曾这样使用过: const Koa=require('koa') const app=new Koa() const Router=require('koa-router') const router=new Router() const cor

flying meng的菜鸟居 7181

【源码】koa-compose洋葱模型原理解析---函数多层调用怎么写更优雅?

资料准备 【若川】koa 洋葱模型实现:https://juejin.cn/post/7005375860509245471 【函数式编程指北】:https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/ch5.html https://www.yuque.com/docs/share/0268760e-60bf-4278-871e-c1e83a68be7a # 克隆git clone https://github.com

rbgg_mp的博客 453

Koa中间件(middleware)实现探索

说起Node,最常用的估计就是express和koa,两者都用到了中间件(middleware)这一概念,主要用于对请求的统一处理。koa的请求处理是典型的洋葱模型,下面是官方的配图,而这一模型的组成部分就是middleware 接下来我们来看一下koa的源码,了解中间件实现方式。

u012362458的博客 1100
上一篇: 《vue项目》《node项目》你们更需求哪个?
下一篇: 记一次Node和Go的性能测试
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值