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(</span>server started at http://localhost:<span class="pl-s1"><span class="pl-pse">${</span><span class="pl-c1">PORT</span><span class="pl-pse">}</span></span><span class="pl-pds">);

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(</span><span class="pl-s1"><span class="pl-pse">${</span><span class="pl-smi">ctx</span>.<span class="pl-c1">method</span><span class="pl-pse">}</span></span> <span class="pl-s1"><span class="pl-pse">${</span><span class="pl-smi">ctx</span>.<span class="pl-smi">url</span><span class="pl-pse">}</span></span> - <span class="pl-s1"><span class="pl-pse">${</span>ms<span class="pl-pse">}</span></span>ms<span class="pl-pds">);

});

});

来自官方 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) =&gt; {
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 中间件的执行的更多相关文章

  1. koa 基础(八)koa 中间件的执行顺序

    1.koa 中间件的执行顺序 app.js /** * koa 中间件的执行顺序 */ // 引入模块 const Koa = require('koa'); const router = requi ...

  2. Koa 中间件的执行顺序

    中间件工作原理 初始化koa实例后,我们会用use方法来加载中间件(middleware),会有一个数组来存储中间件,use调用顺序会决定中间件的执行顺序. 每个中间件都是一个函数(不是函数将报错), ...

  3. koa 中间件

    什么是 Koa 的中间件 通俗的讲:中间件就是匹配路由之前或者匹配路由完成做的一系列的操作,我们就可以 把它叫做中间件. 在express中间件(Middleware)是一个函数,它可以访问请求对象( ...

  4. koa2入门--03.koa中间件以及中间件执行流程

    //中间件:先访问app的中间件的执行顺序类似嵌套函数,由外到内,再由内到外 //应用级中间件 const koa = require('koa'); var router = require('ko ...

  5. koa中间件系统原理及koa+orm2实践。

    koa是由 Express 原班人马打造的新的web框架.套用其官方的说法:Koa 应用是一个包含一系列中间件 generator 函数的对象. 这些中间件函数基于 request 请求以一个类似于栈 ...

  6. KOA中间件的基本运作原理

    示例代码托管在:http://www.github.com/dashnowords/blogs 博客园地址:<大史住在大前端>原创博文目录 华为云社区地址:[你要的前端打怪升级指南] 在中 ...

  7. 傻瓜式解读koa中间件处理模块koa-compose

    最近需要单独使用到koa-compose这个模块,虽然使用koa的时候大致知道中间件的执行流程,但是没仔细研究过源码用起来还是不放心(主要是这个模块代码少,多的话也没兴趣去研究了). koa-comp ...

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

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

  9. Koa - 中间件(理解中间件、实现一个验证token中间件)

    前言 Koa 应用程序是一个包含一组中间件函数的对象,它是按照类似堆栈的方式组织和执行的. 当一个中间件调用 next() 则该函数暂停并将控制传递给定义的下一个中间件.当在下游没有更多的中间件执行后 ...

随机推荐

  1. 我想外包开发一个APP,需要多少钱,多少时间?

    在一个阳光明媚的下午,我正瘫坐在椅子上改bug.忽然有人给我发微信:“我想做个app,多长时间,多少钱?” 从我从业iOS开发到现在,这个问题被问过无数次,比那句:“你是程序员,那你会修电脑吗?”还要 ...

  2. 微信小程序的坑(持续更新中)

    参与微信小程序开发有一段时间了,先后完成信息查询类和交易类的两个不同性质的小程序产品的开发:期间遇到各种各样的小程序开发的坑,有的是小程序基础功能不断改进完善而需要业务持续的适配,有的是小程序使用上的 ...

  3. IM开发基础知识补课(七):主流移动端账号登录方式的原理及设计思路

    1.引言 在即时通讯网经常能看到各种高大上的高并发.分布式.高性能架构设计方面的文章,平时大家参加的众多开发者大会,主题也都是各种高大上的话题——什么5G啦.AI人工智能啦.什么阿里双11分分钟多少万 ...

  4. Linux中fuser命令用法详解

    描述: fuser可以显示出当前哪个程序在使用磁盘上的某个文件.挂载点.甚至网络端口,并给出程序进程的详细信息. fuser显示使用指定文件或者文件系统的进程ID. 默认情况下每个文件名后面跟一个字母 ...

  5. 第11章 UDP:用户数据报协-----读书笔记

    1.分片应用程序只关心IP数据报的长度,如果它超过MTU值,那么就要对数据包进行分片. 2.UDP首部字段图: (16位源端口号+16位目端口号+16位UDP长度+16位UDP校验和+数据) 3.UD ...

  6. Oracle - SPM固定执行计划(二)

    一.前言 前面文章(https://www.cnblogs.com/ddzj01/p/11365541.html)给大家介绍了当一条sql有多个执行计划时,如何通过spm去绑定其中一条执行计划.本文将 ...

  7. ASP.NET Core 3.0 gRPC 配置使用HTTP

    前言 gRPC是基于http/2,是同时支持https和http协议的,我们在gRPC实际使用中,在内网通讯场景下,更多的是走http协议,达到更高的效率,下面介绍如何在 .NET Core 3.0 ...

  8. Reuse Implemented Functionality 重用实现功能

    A default XAF solution contains one platform-agnostic (shared) module and platform-dependent modules ...

  9. PEMDAS 操作順序

    關於計算子 Operator 的操作順序,在"像計算機科學家一樣思考Python"這書 [1] 寫的明白扼要.它以 PEMDAS 這幾個簡單的英文字開頭表明: P (Parenth ...

  10. 033.[转] Java 工程师成神之路 | 2019正式版

    Java 工程师成神之路 | 2019正式版 原创: Hollis Hollis 2月18日 https://mp.weixin.qq.com/s/hlAn6NPR1w-MAwqghX1FPg htt ...