Koa 中间件的执行

Node.js 中请求的处理

讨论 Koa 中间件前,先看原生 Node.js 中是如何创建 server 和处理请求的。

node_server.js

const http = require("http");
const PORT = 3000;const server = http.createServer((req, res) => {res.end("hello world!");
});server.listen(PORT);
console.log(`server started at http://localhost:${PORT}`);

Koa 中请求的处理

Koa 也是通过上面的 http.createServer 创建服务器处理请求的返回 res。 但在 Koa 的封装体系下,其提供了十分好用的中间件系统,可对请求 req 及返回 res 进行便捷地处理。

koa/lib/application.js#L64

  listen(...args) {debug('listen');
+    const server = http.createServer(this.callback());return server.listen(...args);}

Koa 中的 hello world:

server.js

const Koa = require("koa");
const app = new Koa();app.use(async ctx => {ctx.body = "Hello World";
});app.listen(3000);

Koa 中,涉及到对请求返回处理都是通过中间件完成的,像上面为样,返回页面一个 Hello World 文本,也是调用 app.useApplication 对象注册了个中间件来完成。

Koa 中间件编写及使用

Koa 中中间件即一个处理请求的方法,通过调用 app.use(fn) 后,中间件 fn 被保存到了内部一个中间件数组中。

koa/lib/application.js#L105

use(fn) {if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');if (isGeneratorFunction(fn)) {deprecate('Support for generators will be removed in v3. ' +'See the documentation for examples of how to convert old middleware ' +'https://github.com/koajs/koa/blob/master/docs/migration.md');fn = convert(fn);}debug('use %s', fn._name || fn.name || '-');this.middleware.push(fn);return this;}

通过上面的代码可看到,注册的中间件被压入 Application 对象的 this.middleware 数组。这里有对传入的方法进行判断,区分是否为生成器([generator])方法,因为较早版本的 Koa 其中间件是通过生成器来实现的,后面有 async/await 语法后转向了后者,所以更推荐使用后者,因此这里有废弃生成器方式的提示。

因为中间件中需要进行的操作是不可控的,完全有可能涉及异步操作,比如从远端获取数据或从数据库查询数据后返回到 ctx.body,所以理论上中间件必需是异步函数。

比如实现计算一个请求耗时的中间件,以下分别是通过普通函数配合 Promise 以及使用 async/await 方式实现的版本:

来自官方 README 中使用 Promise 实现中间件的示例代码

// Middleware normally takes two parameters (ctx, next), ctx is the context for one request,
// next is a function that is invoked to execute the downstream middleware. It returns a Promise with a then function for running code after completion.app.use((ctx, next) => {const start = Date.now();return next().then(() => {const ms = Date.now() - start;console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);});
});

来自官方 README 中使用 async/await 实现中间件的示例代码

app.use(async (ctx, next) => {const start = Date.now();await next();const ms = Date.now() - start;console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

可以看到,一个中间件其签名是 (ctx,next)=>Promise,其中 ctx 为请求上下文对象,而 next 是这样一个函数,调用后将执行流程转入下一个中间件,如果当前中间件中没有调用 next,整个中间件的执行流程则会在这里终止,后续中间件不会得到执行。以下是一个测试。

server.js

app.use(async (ctx, next) => {console.log(1);next();
});
app.use(async (ctx, next) => {console.log(2);
});
app.use(async (ctx, next) => {console.log(3);ctx.body = "Hello, world!";
});

执行后控制台输出:

$ node server.js
1
2

访问页面也不会看到 Hello, world! 因为设置响应的代码 ctx.body = "Hello, world!"; 所在的中间件没有被执行。

compose

下面来看当多次调用 app.use 注册中间件后,这些中间件是如何被顺次执行的。

中间件的执行是跟随一次请求的。当一个请求来到后台,中间件被顺次执行,在各中间件中对请求 requestresposne 进行各种处理。

所以从 Koa 中处理请求的地方出发,找到中间件执行的源头。

通过查看 lib/application.js 中相关代码:

lib/application.js#L127

  callback() {
+    const fn = compose(this.middleware);if (!this.listenerCount('error')) this.on('error', this.onerror);const handleRequest = (req, res) => {const ctx = this.createContext(req, res);return this.handleRequest(ctx, fn);};return handleRequest;}

可定位到存储在 this.middleware 中的中间件数组会传递给 compose 方法来处理,处理后得到一个函数 fn,即这个 compose 方法处理后,将一组中间件函数处理成了一个函数,最终在 handleRequest 处被调用,开启了中间件的执行流程。

lib/application.js#L151

  handleRequest(ctx, fnMiddleware) {const res = ctx.res;res.statusCode = 404;const onerror = err => ctx.onerror(err);const handleResponse = () => respond(ctx);onFinished(res, onerror);
+    return fnMiddleware(ctx).then(handleResponse).catch(onerror);}

compose 的签名长这样:compose([a, b, c, ...]),它来自另一个单独的仓库 koajs/compose,其代码也不复杂:

koajs/compose/index.js

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!");}/**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */return function(context, next) {// last called middleware #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 {return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));} catch (err) {return Promise.reject(err);}}};
}

这个方法只做了两件事,

  • 定义了一个 dispatch 方法,
  • 然后调用它 dispatch(0)

这里中间件从数组中取出并顺次执行的逻辑便在 dispatch 函数中。

整体方法体中维护了一个索引 index 其初始值为 -1,后面每调用一次 dispatch 会加 1。当执行 dispatch(0) 时,从中间件数组 middleware 中取出第 0 个中间件并执行,同时将 dispatch(i+1) 作为 next 传递到下一次执行。

let fn = middleware[i];
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));

所以这里就能理解,为什么中间件中必需调用 next,否则后续中间件不会执行。

这样一直进行下去直到所有中间件执行完毕,此时 i === middleware.length,最后一个中间件已经执行完毕,next 是没有值的,所以直接 resolve 掉结束中间件执行流程。

if (i === middleware.length) fn = next;
if (!fn) return Promise.resolve();

回到中间件被唤起的地方:

lib/application.js

fnMiddleware(ctx).then(handleResponse).catch(onerror);

中间件完成后,流程到了 handleResponse

总结

从中间件执行流程可知道:

  • 中间件之间存在顺序的问题,先注册的先执行。
  • 中间件中需要调用 next 以保证后续中间件的执行。当然,如果你的中间件会根据一些情况阻止掉后续中间件的执行,那可以不调用 next,比如一个对请求进行权限校验的中间件可以这么写:
app.use(async (ctx, next) => {// 获取权限数据相关的操作...if (valid) {await next();} else {ctx.throw(403, "没有权限!");}
});

相关资源

  • Koa documentation
  • Node.js Documentation - HTTP Class: http.Server
  • MDN - function*
  • koajs/compose

转载于:https://www.cnblogs.com/Wayou/p/koa_middleware.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/483262.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

张亚勤院士谈“智能计算新趋势”

来源&#xff1a;人工智能计算大会作为第四次工业革命的核心&#xff0c;人工智能已经成为全球新一轮科技革命和产业变革的核心驱动力。在2021人工智能计算大会&#xff08;AICC 2021&#xff09;上&#xff0c;清华大学智能科学讲席教授、美国艺术与科学院院士张亚勤带来了《智…

数据结构——排序算法

文章目录1.简单排序1.1 冒泡排序1.2 插入排序2.希尔排序1.简单排序 1.1 冒泡排序 #include<stdio.h>#define ElementType intvoid Swap(ElementType *a,ElementType *b) {ElementType temp;temp*b;*b*a;*atemp; } void Bubble_Sort(ElementType A[],int N) {int p,i;fo…

13、git

安装Git 网上有很多Git安装教程&#xff0c;如果需要图形界面&#xff0c;windows下建议使用TortoiseGit&#xff0c;linux建议使用Git GUI或者GITK。(windows下载exe安装包&#xff0c;linux可以使用命令行&#xff09; 1. Git基本配置 配置用户名及邮箱 在使用Git提交前&…

神经网络支持大脑是「预测机器」,预测是节能的

来源&#xff1a;ScienceAI编译&#xff1a;绿萝我们的大脑&#xff0c;一个包裹在骨性头骨内的三磅重的组织&#xff0c;如何从感觉中产生知觉是一个长期存在的谜。大量证据和数十年的持续研究表明&#xff0c;大脑不能像拼拼图一样&#xff0c;简单地组合感官信息来感知周围环…

数据结构与算法——链表题目实现

文章目录1.链表逆序1.1 题目描述1.2链表逆序的C实现2.反转链表2.1 题目描述2.2 反转链表的C实现3.求两个链表的交点3.1 题目描述3.2 C实现——set3.3 C语言实现——链表长度实现4.链表求环4.1 题目描述4.2 C实现4.3 C语言实现——快慢指针5.分隔链表5.1 题目描述5.2 C实现6.复制…

元宇宙不是下一代互联网,而是人类群体思维空间或梦境世界的具现

前言&#xff1a;本文是根据6G俱乐部举办的6G与元宇宙研讨会上的发言整理形成作者&#xff1a;刘锋目前&#xff0c;业内有一种声音提出元宇宙是下一代互联网&#xff0c;之前WEB2.0、物联网、移动互联网和区块链爆发的时候也曾经这样表达过&#xff0c;如果从互联网的发展历史…

【Redis】三、Redis安装及简单示例

&#xff08;四&#xff09;Redis安装及使用 Redis的安装比较简单&#xff0c;仍然和大多数的Apache开源软件一样&#xff0c;只需要下载&#xff0c;解压&#xff0c;配置环境变量即可。具体安装过程参考&#xff1a;菜鸟教程Redis安装。 安装完成后&#xff0c;通过redis-ser…

数据结构和算法——栈、队列、堆

文章目录1.预备知识1.1 栈1.2 队列1.3 堆2.用队列实现栈2.1 题目描述2.2 解题思路2.3 C实现3.用栈实现队列3.1 题目描述3.2 解题思路3.3 C实现4.最小栈4.1 题目描述4.2 解题思路5.合法的出栈序列5.1 题目描述5.2 解题思路5.3 C实现6.基本计算器6.1 题目描述6.2 解题思路7.数组中…

LeetCode 刷题笔记 (树)

1. minimum-depth-of-binary-tree 题目描述 Given a binary tree, find its minimum depth.The minimum depth is the number of nodes along the shortest path from the root node down to the nearest leaf node.求二叉树的最小深度&#xff1b;解题思路 递归遍历二叉树的每…

综述 | 北斗系统应用趋势分析

来源&#xff1a;智绘科服初审&#xff1a;张艳玲复审&#xff1a;宋启凡终审&#xff1a;金 君一、前言2020年6月23日&#xff0c;北斗三号最后一颗组网卫星成功发射。2020年7月31日&#xff0c;北斗三号建成暨开通仪式举行&#xff0c;北斗三号全球卫星导航系统正式开通[1]…

数据结构与算法——贪心算法

文章目录1.分发饼干1.1 题目描述1.2 解题思路1.3 C实现2.摆动序列2.1 题目描述2.2 解题思路2.3 C实现3.移掉K位数字3.1 题目描述3.2 解题思路3.3 C实现4.跳跃游戏4.1 题目描述4.2 解题思路4.3 C实现5.跳跃游戏 II5.1 题目描述5.2 解题思路5.3 C实现6.用最少数量的箭引爆气球6.1…

人为什么要睡觉?科学家给出进一步答案

来源&#xff1a;科技日报作者&#xff1a;张佳欣 人类一生中有三分之一的时间在睡觉&#xff0c;包括苍蝇、蠕虫甚至水母等无脊椎动物也会睡觉。在整个进化过程中&#xff0c;睡眠对所有具有神经系统的有机体来说都是普遍的&#xff0c;也是必不可少的。然而你有没有想过&…

自我觉察-3:发现-我这么做究竟为了什么?

今天一个人在食堂吃饭时&#xff0c;不是很饿&#xff0c;又拿了很多菜&#xff0c;就挑挑拣拣的吃&#xff0c;然后呢&#xff0c;又觉得吃相不雅&#xff0c;让别人看了不好。就有点儿端着架子吃似的。这时候&#xff0c;我突然想起一句话“我用自认为优雅地姿势吃饭为了什么…

操作系统——简介

文章目录1.操作系统的功能和目标1.1 作为用户和计算机硬件之间的接口1.2 作为系统资源的管理者1.3 作为最接近硬件的层次2.操作系统的概念、功能和目标3.操作系统的四大特征3.1 并发3.2 共享3.3 虚拟3.4 异步4.操作系统的运行机制4.1 两种指令4.2 两种处理器状态4.3 两种程序5.…

Andrew Gelman、Aki Vehtari​ | 过去50年最重要的统计学思想是什么?

来源&#xff1a; 数据分析网作者 &#xff1a;Andrew Gelman 美国统计学家、哥伦比亚大学统计学教授Aki Vehtari 阿尔托大学计算机科学系副教授近日&#xff0c;图灵奖得主、“贝叶斯网络之父”Judea Pearl在Twitter上分享了一篇新论文“What are the most important statis…

全局唯一ID的生成

数据在分片时&#xff0c;典型的是分库分表&#xff0c;就有一个全局ID生成的问题。单纯的生成全局ID并不是什么难题&#xff0c;但是生成的ID通常要满足分片的一些要求&#xff1a; 1 不能有单点故障。 2 以时间为序&#xff0c;或者ID里包含时间。这样一是可以少一个索引…

操作系统——进程

文章目录1.进程的定义2.进程的组成3.PCB4.进程的状态4.1 进程的五种状态4.2 进程状态间的转换5.进程控制6.进程通信6.1 共享存储6.2 管道通信6.3 消息传递7.线程7.1 线程的概念7.2 引入线程后的变化7.3 线程的属性7.4 线程的实现方式7.4.1 用户级线程7.4.2 内核级线程7.4.3 混合…

10分钟了解图卷积神经网络的常用算法和发展方向

来源&#xff1a;数学算法俱乐部近几年&#xff0c;机器学习在各个领域井喷式发展&#xff0c;现已成为当下最热门的技术。掌握机器学习&#xff0c;你就比 80% 的人更具备竞争优势。谷歌的无人驾驶、抖音的推荐系统、百度的人脸识别、大疆的无人机、科大讯飞的语音识别、小米的…

python学习之数据类型(int,bool,str)

第三章 数据类型 3.1 Python基本数据类型 类型含义描述int整数主要用来进⾏数学运算str字符串可以保存少量数据并进⾏相应的操作bool布尔值判断真假&#xff0c;True&#xff0c;Falselist列表存储⼤量数据&#xff0c;⽤[ ]表示tuple元组不可以发⽣改变 用( )表示dict字典保存…

操作系统——调度

文章目录1.调度的概念2.调度的三个层次2.1 高级调度2.2 中级调度2.3 低级调度2.4 三种调度之间的关联1.调度的概念 2.调度的三个层次 2.1 高级调度 2.2 中级调度 2.3 低级调度 2.4 三种调度之间的关联