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. (day65、66)Vue基础、指令、实例成员、JS函数this补充、冒泡排序

    目录 一.Vue基础 (一)什么是Vue (二)为什么学习Vue (三)如何使用Vue 二.Vue指令 (一)文本指令 (二)事件指令v-on (三)属性指令v-bind (四)表单指令v-model ...

  2. [考试反思]1113csp-s模拟测试113:一念

    在这么考下去可以去混女队了2333 两天总分rank14,退役稳稳的 的确就是没状态.满脑子都是<包围保卫王国>ddp/LCT/ST,没好好考试. 我太菜了写题也写不出来考试也考不好(显然 ...

  3. HashMap如何在Java中工作?

    通过优锐课学习笔记分享,我们可以看到HashMap问题在工作面试中很常见. 这也是HashMaps在Java内部如何工作的一些深入说明,分享给大家参考学习. HashMap在内部如何工作已成为几乎所有 ...

  4. 【Unity游戏开发】Android6.0以上的动态权限申请问题

    一.引子 最近公司的游戏在做安全性测试,期间也暴露出了不少安全上的问题.虽然我们今天要说的权限申请和安全性相关不大,但是也会影响到游戏的使用体验等,所以本篇博客中马三就想和大家谈谈Android6.0 ...

  5. javascript截取字符串的最后几个字符

    在JavaScript中截取字符串一般是使用内置的substring()方法和substr()方法,这两个方法功能都很强大,也都能实现截取字符串中的最后几个字符. substring()方法 Java ...

  6. 解决spark dataframe get 报空指针异常 java.lang.NullPointerException

    Spark 编程读取hive,hbase, 文本等外部数据生成dataframe后,一般我们都会map遍历get数据的每个字段,此时如果原始数据为null时,如果不进行判断直接转化为string,就会 ...

  7. [IDA] 自动下载符号

    当现实无法自动下载符号时,看下面交互窗口,提示安装 VC++ 2008. 安装成功之后就会自动下载符号.

  8. The server time zone value '�й���׼ʱ��' is unrecognized or represents more than one time zone.

    介绍 再使用spring操作mysql数据库报错 @Test public void test() { try { //创建连接池,先使用spring框架内置的连接池 DriverManagerDat ...

  9. 解决Xcode10 Library not loaded: /usr/lib/libstdc++.6造成的crash及报错

    关键字1:dyld: Library not loaded: /usr/lib/libstdc++.6.dylib   Referenced from: 关键字2:Reason: no suitabl ...

  10. 解决MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk.问题

    突然发现昨天刚搭建的websocket不能连接了,提示: MISCONF Redis is configured to save RDB snapshots, but it is currently ...