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. pyhon代码设计格式指南

    一 缘由: 经常看到周围人写的代码,在大公司有规范,很多人不乱写代码,但是在很多小公司,很多程序员都是半路出家,也不喜欢读书学习,导致写出来的代码乱七八糟.今天拜读了python编程这本书,作者把py ...

  2. 【c&c++】变量初始化

    /* 全局变量会默认初始化为0 空 和 false,局部变量要手动初始化,或者直接scanf输入值进去.如果不初始化,n可能是0也有可能是其他数,大多数情况下会是0,但不初始化是不安全的,所以Dev直 ...

  3. C++ 如何用百行代码实现线程安全的并发队列 | concurrent queue or blocking queue implemented in cpp

    本文首发于个人博客https://kezunlin.me/post/cabccf5c/,欢迎阅读最新内容! concurrent queue or blocking queue implemented ...

  4. Java 后台请求第三方系统接口详解

    //调用第三方系统接口 PrintWriter out = null; BufferedReader in = null; JSONObject jsonObject = null; Closeabl ...

  5. 在IntelliJ IDEA中,Lombok注解@Slf4j找不到log解决方案

    在IntelliJ IDEA中,注解@Slf4j找不到log时,可以安装Lombok插件 File → settings → Plugins, 然后点击"Browse repositorie ...

  6. 项目部署到Linux上遇到的坑

    作者:晨钟暮鼓c个人微信公众号:程序猿的月光宝盒 1.本地Navicat for MySQL无法连接至服务器(Centos 7 x86_64 bbr) 1045错误: 解决步骤: ​ 1.查看用户名密 ...

  7. python爬虫爬小说网站涉及到(js加密,CSS加密)

    我是对于xxxx小说网进行爬取只讲思路不展示代码请见谅 一.涉及到的反爬 js加密 css加密 请求头中的User-Agent以及 cookie 二.思路 1.对于js加密 对于有js加密信息,我们一 ...

  8. 【SDUT】2019SDUTACM第一次选拔赛 F- X的追求道路

    Problem Description X在大家的帮助下终于找到了一个妹纸,于是开始了漫漫的追求之路,那么大家猜一猜X能不能追的上呢? X初始对妹纸有一个心动值,妹纸对X有一个好感值,在追求时发生的的 ...

  9. Java - 包装类 常量池

    概述: 在Java中存在一些基本数据类型,这些基本数据类型变量,不能像其他对象一样调用方法,属性.... 一些情况下带来一些问题,包装类就是为了解决这个问题而出现 包装类可以使得这些基础数据类型,拥有 ...

  10. 松软科技web课堂:JavaScript 数组迭代方法

    数组迭代方法对每个数组项进行操作,听着挺高深,其实,就是对数组对象一次性逐一进行一种操作的一种叫法.(文章来源:www.sysoft.net.cn,加v:15844800162深度交流) Array. ...