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

Koa 中间件的执行相关推荐

  1. 深入理解 Koa 中间件之 “ 洋葱模型 ”

    欢迎关注我的公众号『 前端我废了 』,查看更多文章!!! 前言 我们知道创建一个 Koa 应用主要分三步: const Koa = require('koa'); // 1. 创建一个 Koa 实例 ...

  2. 什么是koa中间件,他们的执行顺序是什么样的?

    koa中间件 koa中,中间件分为应用级和路由级 //应用级 app.use(async (ctx, next) => { //应用级中间件 ,先执行中间件,再匹配路由console.log(& ...

  3. 初识洋葱模型,分析中间件执行过程,浅析koa中间件源码

    前言 作为洋葱模型的第一篇文章,这里仅介绍了一些入门级知识,比如 了解洋葱模型执行顺序 分析部分 koa 中间件的源码来加深对中间件的认识 为第二篇文章:分析洋葱模型实现原理,在自己项目中接入洋葱模型 ...

  4. egg koa 中间件执行原理,洋葱模型原理

    1.官网例子 const Koa = require('koa'); const app = new Koa();// logger app.use(async (ctx, next) => { ...

  5. koa 接口返回数据_一文搞定 Koa 中间件实现原理

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

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

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

  7. generator探幽(1)--koa中间件机制浅析

    本系列旨在通过对co,koa等库源码的研究,进而理解generator在异步编程中的重大作用(ps:所有代码请在node --harmony或者iojs环境中运行) koa中间件的形式 相信用过koa ...

  8. 【nodejs原理源码赏析(2)】KOA中间件的基本运作原理

    [摘要] KOA中间件的基本运作原理 示例代码托管在:http://www.github.com/dashnowords/blogs 在中间件系统的实现上,KOA中间件通过async/await来在不 ...

  9. koa 中间件洋葱模型源码分析

    中间件基本使用 app.use(async(ctx,next)=>{ctx.state={username:'jeff'};await next();... })app.use(async(ct ...

最新文章

  1. oracle 游标小例
  2. Web UI回归测试 -- BackstopJS 入门
  3. Solr+Hbase多条件查(优劣互补)
  4. 为什么要选择Hibernate
  5. 追了多年的开发框架,你还认识指针吗?
  6. 英特尔cpu发布时间表_英特尔10nm芯片开始大规模出货,先进制程时间表浮出水面...
  7. mysql slow log 分析工具_mysql slow log分析工具的比较
  8. 计算机技术在机械设计中的应用,计算机技术在机械设计制造和自动化中的应用(原稿)...
  9. 为什么说电商创业的机会在变少?
  10. php实现从尾到头打印列表
  11. 泛型list集合类转换成DataTable、datatable转list
  12. linux播放csf文件
  13. 洛谷P5369 [PKUSC2018]最大前缀和 [DP]
  14. 服务器修改硬盘顺序,服务器硬盘阵列硬盘顺序
  15. android7工程自测模式,工程模式测试
  16. 什么是TDK?什么是网站的TDK?扫(myself的)盲
  17. ArcGIS的栅格数据空间分析——栅格插值(1)
  18. 数据资产价值评估与定价:研究综述和展望
  19. jsp servlet mysql项目_JSP+Servlet+JDBC+mysql实现的个人日记本系统
  20. 欧飞信科技ELEXCON 2022深圳国际电子展精彩回顾

热门文章

  1. 计量经济学实验报告计算机,计量经济学-实验报告.doc
  2. 安卓 camera 调用流程_安卓如何做出微信那样的界面仿微信“我”的界面1/5
  3. 1+X web中级 Laravel学习笔记——blade模版
  4. Node 中的开发环境与生产环境 和 使用Morgan打印请求信息
  5. 剑指Offer - 面试题50. 第一个只出现一次的字符(unordered_map)
  6. Linux单用户能做什么,Linux单用户模式详解 及应用场景
  7. 利用python批量修改文件名称
  8. python数据分析开发环境_在MAC上搭建python数据分析开发环境
  9. python网络编程内容_Python网络编程
  10. 消息中间件系列(五):MQ消息队列的12点核心原理总结