欢迎关注我的公众号『 前端我废了 』,查看更多文章!!!

前言

我们知道创建一个 Koa 应用主要分三步:

const Koa = require('koa');
// 1. 创建一个 Koa 实例
const app = new Koa();// 2,加载多个中间件
app.use(/*中间件*/);
// ... 其他中间件// 3. 指定服务器端口,创建一个 http 服务器
app.listen(3000);

那么中间件的执行顺序是怎样的呢?执行顺序是如何生成的呢?这篇文章就让我们来一探究竟。

koa 中间件的执行顺序

如下示例代码,例如使用中间件 x-response-timeloggerresponse

const Koa = require('Koa');
const app = new Koa();// 中间件1 x-response-time
app.use(async (ctx, next) => {const start = Date.now();await next();const ms = Date.now() - start;ctx.set('X-Response-Time', `${ms}ms`);
});// 中间件2 logger
app.use(async (ctx, next) => {const start = Date.now();await next();const ms = Date.now() - start;console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});// 中间件3 response
app.use(async ctx => {ctx.body = 'Hello World';
});app.listen(3000);

上面代码中间件的执行顺序如下动图

以上中间件的执行顺序,我们会常听到一个词叫做 “洋葱模型”(如下图),何为“洋葱模型“?洋葱内的每一层表示一个独立的中间件,用于实现不同的功能,比如日志记录,异常处理等。每次请求都会从左侧最外层开始,一层层经过中间件,当执行到最里层的中间件之后,接着从最里层的中间件开始逐层返回。因此对于每层的中间件来说,在一个 请求和响应 周期中,都有两个时机点来添加不同的处理逻辑。是不是有点像 DOM 事件流的事件捕获阶段(从外到里)和事件冒泡阶段(从里到外)。

上面中间件的执行顺序是怎么生成的呢?从 Koa 源码入手,从 Koa 源码的 `package.json 中 main 字段得知入口文件指向的是 lib/application.js 文件,详解一下创建一个 koa 应用执行过程:

  1. const app = new Koa() ;我们 new Koa() 其实就是创建类 Application 的实例;
  2. app.use(/*中间件*/);当我们调用 app.use(function) 加载中间件时,use 函数内部将中间件函数都保存到了 middleware 数组里面;
  3. app.listen(/* 端口 */),内部使用 http.createServer() 创建一个 http 服务器,并调用 this.callback() 的返回值作为参数;callback 函数内部调用 compose 函数(即 koa-compose 中间件),就是造就中间件执行顺序的"幕后黑手"。

下面我们逐行解析 koa-compose 源码,看看是如何生成 “洋葱模型”的。

koa-compose 源码逐行详解

koa-compose 源码(index.js 文件)内部导出的就是 compose 函数,我们除了看源码,也可以结合对应测试用例(test.js 文件)来理解。

/*** compose 函数接收一个中间件数组,返回一个中间件函数* @param {Array} middleware* @return {Function}*/
function compose (middleware) {// 若传入的参数 middleware 不是数组,则抛错if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')// 若 middleware 数组项不是函数,则抛错for (const fn of middleware) {if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')}/*** 返回一个中间件函数,第一参数为一个请求的上下文对象;第二个参数 next 函数为调用下一个中间件的函数*/return function (context, next) {// 当前中间件在 middleware 数组中的索引位置let index = -1// 返回 dispatch(0) 结果return dispatch(0)// 该函数目的递归调用中间件,生成中间件嵌套调用结构function dispatch (i) {// 每个中间件函数内部只能调用一次 next 函数,否则抛错// 测试用例 should throw if next() is called multiple times if (i <= index) return Promise.reject(new Error('next() called multiple times'))// 记录索引index = i// 取出当前中间件函数let fn = middleware[i]// 若当前索引值等于中间件数组的长度,即 middleware 数组的中间件都处理完了,则 fn 赋值为参数 nextif (i === middleware.length) fn = next// 若 fn 不存在,则 resolve 掉,结束if (!fn) return Promise.resolve()try {// 返回 Promise,执行中间件函数 fn,这里将下一个中间件函数执行器作为第二个参数传入当前中间件,目的是将下一个中间件函数的执行权交由当前中间件,在其内部手动调用;// 这里利用 bind 函数来实现,bind 函数执行后会返回一个新的函数,并不会立即执行,什么时候执行呢?也就是当前中间件 fn 函数里的 await next() 执行时,此时这个 next 函数也就是现在 fn 函数传入的第二个参数 dispatch.bind(null, (i + 1)的返回值return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))} catch (err) {return Promise.reject(err)}}}
}

假如有三个中间件分别为 fn1fn2fn3,那么在 Koa 内部经过中间件 koa-compose 组合后,将会生成如下的嵌套结构

// compose 函数返回值也是一个中间件函数
const fn = compose([fn1, fn2, fn3])
// compose 返回的函数 fn 内部嵌套结构,简略代码
function(context){return Promise.resolve(fn1(context, function next(){return Promise.resolve(fn2(context, function next(){return Promise.resolve(fn3(context, function next(){return Promise.resolve();}))}))}));
};

以上就是生成中间件执行顺序的代码,核心就是 dispatch 函数,通过递归生成中间件嵌套调用结构,将下一个中间件的控制权通过函数参数的形式传递给当前中间件,以此类推,生成类似洋葱模型结构的执行顺序。

扩展:Koa 与 Express 对比

  1. express 拥有路由、模板等框架常见功能,Koa 不含任何中间件,Koa 可被视为 node.js 的 http 模块的抽象,Express 则是 node.js 的应用程序框架。

  2. 中间件实现机制;express 基于 Callback,koa 基于 Promise

  3. 错误处理;express 对错捕获处理起来很不友好,每一个回调都拥有一个新的调用栈,因此你没法对一个 callback 做 try catch 捕获,你需要在 Callback 里做错误捕获,然后一层一层向外传递。

  4. 响应机制;express在调用 res.send 方法后就立即响应了,而koa则是在所有中间件调用完成之后,在最外层中间件进行响应。

    引用一段其他网友总结的 xpress 和 koa 中间件机制的不同:

    其实中间件执行逻辑没有什么特别的不同,都是依赖函数调用栈的执行顺序,抬杠一点讲都可以叫做洋葱模型。Koa 依靠 async/await(generator + co)让异步操作可以变成同步写法,更好理解。最关键的不是这些中间的执行顺序,而是响应的时机,Express 使用 res.end() 是立即返回,这样想要做出些响应前的操作变得比较麻烦;而 Koa 是在所有中间件中使用 ctx.body 设置响应数据,但是并不立即响应,而是在所有中间件执行结束后,再调用 res.end(ctx.body) 进行响应,这样就为响应前的操作预留了空间,所以是请求与响应都在最外层,中间件处理是一层层进行,所以被理解成洋葱模型,个人拙见。

总结

牛逼!!!

参考

  • 如何更好地理解中间件和洋葱模型

  • 多维度分析 Express、Koa 之间的区别

  • Koa与 express 的中间件机制揭秘

  • Egg.js 与 Koa

  • Koa-vs-express

  • Talk about koa’s onion model

深入理解 Koa 中间件之 “ 洋葱模型 ”相关推荐

  1. 如何更好地理解中间件和洋葱模型

    相信用过 Koa.Redux 或 Express 的小伙伴对中间件都不会陌生,特别是在学习 Koa 的过程中,还会接触到 "洋葱模型". 本文阿宝哥将跟大家一起来学习 Koa 的中 ...

  2. web前端技术分享:koa中间件是如何实现的?

    在前端开发过程中我们可能会使用到koa中间件,但很多同学却不知道它是如何实现的,下面小千就来给大家介绍一下这个koa中间件(洋葱模型). 一.问题分析 async await是promise的语法糖, ...

  3. 洋葱模型php,理解Koa洋葱模型

    中间件特性 | | | middleware 1 | | | | +-----------------------------------------------------------+ | | | ...

  4. 说说你对koa中洋葱模型的理解?

    用过Koa的,肯定对 Middleware(中间件) 有所了解,那我们就用中间件从现象出发,理解洋葱模型 先自定义两个中间件: logTime:打印时间戳 module.exports=functio ...

  5. 你需要掌握的 Koa 洋葱模型和中间件

    大家好,我是前端西瓜哥. Koa 是一个 nodejs 框架,经常用于写 web 后端服务.它是 Express 框架的原班人马开发的新一代 web 框架,使用了 async / await 来优雅处 ...

  6. 聊一聊KOA的洋葱模型

    Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小.更富有表现力.更健壮的基石. 通过利用 async 函数,Koa ...

  7. KOA 初探 洋葱模型

    KOA 官网链接 当然,如果你有express的基础就最好了,毕竟是原班人马开发 常用的指令 npm install koa npm install nodemon touch app.js node ...

  8. 【Koa】为什么一定要保证是洋葱模型呢?

    为什么一定要保证是洋葱模型呢? const Koa = require('koa');const app = new Koa(); /*聊一聊为什么我们通常需要在koa的中间件加上async,如果这个 ...

  9. 洋葱模型php,聊一聊KOA的洋葱模型

    Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造, 致力于成为 web 应用和 API 开发领域中的一个更小.更富有表现力.更健壮的基石. 通过利用 async 函数,Koa ...

最新文章

  1. mysql集群安装配置
  2. 前端-----盒子模型
  3. OpenHarmony的多内核
  4. 转jmeter --JDBC请求
  5. python中的栈结构_Python可以实现栈的结构吗
  6. logstash 获取多个kafka_logstash 配置详解
  7. .NET 5.0 RC1 发布,离正式版发布仅剩两个版本,与 netty 相比更具竞争力
  8. python之路--day10-闭包函数
  9. python画三维图-Python+matplotlib绘制三维图形5个精选案例
  10. IdentityServer4支持的授权类型以及组合
  11. html页面跳转方式 + 跳转传参
  12. [转]Spring 注解总结
  13. python贝叶斯网络预测模型_概率图模型之:贝叶斯网络
  14. AD15如何在PCB界面锁定选中元件
  15. 第十届泰迪杯数据挖掘B题电力系统负荷预测分析
  16. 高德导航java_通过拼接实现高清地图的下载-高德-java实现
  17. 编译安装wpa_supplicant
  18. 诗歌三 不积跬步,无以至千里
  19. Matlab系列教程_基础知识_绘图(一)
  20. c语言指针学多久,C语言指针难学吗?

热门文章

  1. 嵌入式Linux中间件,高可用性(HA)和嵌入式管理中间件:Enea Element详解
  2. centos7配置 console口_7.5. Configuring the Linux Console
  3. 功耗大好还是小好_热设计功耗高好还是低好 - 卡饭网
  4. 中日电脑相关词汇(超详版)
  5. 如果你没读懂《骇客帝国》
  6. 关于成功人士成功秘诀的乱弹琴
  7. imvu为什么显示无法连接服务器,IMVU服务器错误怎么办 服务器无法连接解决办法...
  8. 香港机房中的BGP线路具有哪些优势
  9. Linux Shell脚本语句执行失败,后续语句继续执行的问题
  10. 《那些年入上百万的人是如何做到的》读后感