理解Express的middleware

什么是middleware

Express有两个核心概念:middleware和routing,也是使得Express应用模块化、组织清晰、可维护性高的关键。本篇先来讲讲middleware。

Middleware,可以翻译成“中间件”,其本质就是一个处理request的方法,但是单个middleware并不完成所有的逻辑。打个比方,原生Node中处理request的逻辑是铁板一块,而Express则将其重组成一长根链条,每一环都是一个middleware、负责处理好一小部分(do one thing well),依次来完成整个处理逻辑。

原生Node和Express处理客户端请求的逻辑的对比

那么middleware到底是什么呢?简单来说,在代码中,middleware实际上就是一个方法,它接受代表请求和应答的两个对象(由Node内置的http这个模块创建,由Express增强)。但此外,它还接受一个参数,这个参数代表了栈中下一个应该执行的middleware。即是说,middleware的代码一般都长成下面这个样子:

function aMiddleware(request, response, next) {
    // ...
    next();
}

注意最后需要手动调用下一个middleware(next()),否则请求的处理过程会被挂起,直到超时。

所以,对Express应用来说,整个图景是这个样子:

[图片上传失败...(image-4451c-1517737496791)]

光讲理论不太好理解,下面来看看实际上如何用Express来实现一个的简单应用。

首先新建一个应用文件夹:

$ mkdir hello-express
$ cd hello-express

安装Express:

$ npm install --save express

在根目录下新建app.js文件,写入以下内容:

// 导入内置模块http
var http = require('http');

// 导入第三方模块express
var express = require('express');

// 定义负责log的middleware
function logger(request, response, next) { 
    console.log(request.method + ': ' + request.url); 
    next();// 调用下一个middleware
}

// 定义负责响应的middleware
function responser(request, response) {
    if (request.url === '/') {
        return response.end('Welcome to Homepage!');
    }
    if (request.url === '/about') {
        return response.end('Welcome to About Page!');
    }
    response.end('404, Page Not Found!');
}

// 创建Express应用的对象
var app = express();

// 组建middleware栈,注意顺序
app.use(logger);
app.use(responser);

var server = http.createServer(app);
server.listen(3000);

保存,运行node app.js,浏览器访问http://localhost:3000 (或者同一host的不同path),能看到应该返回的信息。

上面的代码中的要点:

  1. 首先要导入express模块,并创建Express应用的对象
  2. 使用use方法来按顺序“登记”所定义的middleware。当收到客户端请求后,请求对象会依登记的顺序通过整个middleware栈(简单来说)
  3. 注意在logger中手动调用了下一个middleware,但是在responser中没有,这是因为我们知道它是最后一个middleware,所以可以省略参数与调用步骤

Middleware深入

通过上一节,我们了解了下面两个事实:

  1. Express应用实际上就是一连串middleware的组合
  2. 一般来讲,middleware就是一个方法,其参数为request对象、response对象和下一个middleware

实际上,上面对middleware的定义并不正确,因为(下面马上会说到)middleware也可能是一个接受四个参数的方法、甚至不是方法。

不妨这么定义:middleware是Express应用的逻辑单元;如果把从收到客户端请求到回复应答的过程称为“请求-应答回合”的话,那么一个middleware可能有如下四个功能:

  • 执行不更改request和response对象的逻辑,比如之前的logger,它仅仅在命令行打印一段日志,并不处理request和response
  • 更改request和/或response对象
  • 终结某个“请求-应答回合”,比如调用response.end(...)
  • 调用下一个middleware

换句话说,middleware就是能够传给app.use()方法、负责一部分应用逻辑的东西

按照其本质,middleware可以分为三类:

  1. application-level middleware(应用级别中间件)
  2. router-level middleware(分路级别中间件)
  3. error-handling middleware(处理错误中间件)

其中,application-level middleware就是常见的接受三个参数的方法((req, res, next) => {...});error-handling middleware则接受四个参数((req, res, next, error));而router-level middleware实际上并非方法,而是一个express.Router对象。

按照来源,middleware又可以分为三类:

  1. 内置的
  2. 第三方的
  3. 应用开发者自写的

这个很好理解。开发Express应用可以理解为:自写middleware,选择使用内置、第三方middleware,并将其组织起来的过程。

下面我们来认识几个常用的第三方/内置middleware。

唯一的内置middleware模块

原生Node在处理客户端向服务器请求静态文件时,会非常麻烦。这一节则来看看Express应用是怎么处理这种情况的。

会到hello-express文件夹,假设和app.js文件所在同一位置有个名为public的文件夹,下面有如下一个名为marigold.jpg的图像文件:

marigold.jpg

那么如何处理客户端对这幅图的处理呢?或者说,如何使得服务器能够向客服端“服务”这个文件呢?

这就要用到Express V4.x唯一的内置middleware模块了:serve-static。因为它是内置模块,所以不用下载和导入任何东西。具体使用的时候,只需告诉该模块这些静态文件所在的文件夹位置,它就会返回一个middleware好让开发者将其加入到应用的middleware栈里。

打开app.js文件,将其内容更改为:

var http = require('http');
var path = require('path');
var express = require('express');

var app = express();

// +++
var publicFilesPath = path.join(__dirname, 'public');
var publicFilesServer = express.static(publicFilesPath);
app.use(publicFilesServer);
// +++

app.use(function responser(request, response) {
    if (request.url === '/') {
        return response.end('Welcome to Homepage!');
    }
    if (request.url === '/about') {
        return response.end('Welcome to About Page!');
    }
    response.end('404, Page Not Found!');
});

var server = http.createServer(app);

server.listen(3000);

上面的代码里值得注意的点为:

  1. path是Node的(而非Express的)一个内置模块,用来处理文件路径
  2. __dirname是当前正在运行的文件所在的地址
  3. 使用path.join而不是简单地__dirname + '/public'是为了兼容Windows和Linux、Mac环境
  4. express.static就是一个serve-static模块
  5. express.static出来接受静态文件的文件夹路径外,还可以接受一个JS Object来定义其行为;这里就不展开了,具体见前面给出的模块文档
  6. 一个Express应用可以用上面的方法定义多个静态文件夹的位置

保存文件,运行程序node app.js,浏览器访问http://localhost:3000/marigold.jpg,则可以看到服务器成功地回应了我们所请求的文件:

[图片上传失败...(image-a5059b-1517737496791)]

第三方middleware模块:morgan

“重新发明轮子”是IT圈的大忌。在写middleware之前,最好看看是不是已经有人帮我们实现了想要的功能。

比如前面的log功能,就可以直接用第三方的morgan模块。(实际上,这个模块也是Express小组维护的,是从以前版本的Express分离出去的。)

回到hello-express文件夹下,安装morgan模块:

$ npm install --save morgan

然后更改app.js文件如下:

var http = require('http');
var express = require('express');
var logger = require('morgan');

var app = express();

var publicFilesPath = path.join(__dirname, 'public');
var publicFilesServer = express.static(publicFilesPath);
app.use(publicFilesServer);

// +++
app.use(logger('short'));
// +++

app.use(function responser(request, response) {
    if (request.url === '/') {
        return response.end('Welcome to Homepage!');
    }
    if (request.url === '/about') {
        return response.end('Welcome to About Page!');
    }
    response.end('404, Page Not Found!');
});

var server = http.createServer(app);

server.listen(3000);

运行程序node app.js,浏览器访问http://localhost:3000,试试不同的path,看看命令行会有怎样的输出。

上面的代码里,logger('short')会返回一个方法,正好可以用来替代前面我们自己写的logger方法。morgan模块还支持其他很多不同的格式,帮开发者记录收到的请求和其他重要信息。比如,开发时一般会使用'dev'模式,详细信息请见其文档

第三方middleware模块:body-parser

body-parser是Express最重要的第三方middleware模块之一。它将客服端发来的HTTP请求体解析成JS对象。这一节我们来说说它的具体用法。

跟morgan一样,首先我们要安装body-parser:

$ npm install --save body-parser

并在代码中导入:

var bodyParser = require('body-parser');

在Express应用中,经常见到这样的使用body-parser模块的方法:

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

bodyParser.json()会返回一个middleware。当来自客户端的HTTP请求体的MIME类型为application/json时(也就是含有头字段Cotent-Type: application/json时),这个middleware就会试着把请求体解析成JS对象。其结果会存给request.body这个对象,以便后面的middleware使用。

bodyParser.json()可以授受一个JS对象,来定义其行为,但这里就不深入了,详见其官方页面文档。

类似地,bodyParser.urlencoded()也返回一个middleware。这个middleware是用来解析URL中的query部分的,它只处理含有x-ww-form-urlencoded头字段的HTTP请求,其结果也会是一个JS对象,存给request.body。值得指出的是,上面代码中的extended选项是必须得提供的,它接受一个布尔值,当设置成true时,这个middleware使用qs模块来解析URL,否则就使用Node的querystring模块。一般来说,推荐设置成{ extended: true }

除了morgan和body-parser,比较常用又十分重要的第三方middleware模块还包括:

  • cookie-parser,用来解析HTTP请求的Cookie头字段,解析成request.cookie
  • serve-farvicon,用来处理对网页图标的请求

这里都不做细讲了,有兴趣的同学可以自行搜索学习。

处理错误的middleware

处理错误的middleware和应用级别的middleware就只有一个差别,那就是它接受四个参数,其签名可以写为(err, req, res, next)=>void

边做边学,我们来看看实际上怎么应用。假设,在刚刚开始写一个网络应用的时候,还没有专门的404页面,而是想把404信息当成错误来处理,则可以像下面这样处理。

回到hello-express文件夹,修改app.js如下:

var http = require('http');
var express = require('express');
var logger = require('morgan');

var app = express();

var publicFilesPath = path.join(__dirname, 'public');
var publicFilesServer = express.static(publicFilesPath);
app.use(publicFilesServer);

app.use(logger('short'));

app.use(function responser(request, response) {
    if (request.url === '/') {
        return response.end('Welcome to Homepage!');
    }
    if (request.url === '/about') {
        return response.end('Welcome to About Page!');
    }
    response.end('404, Page Not Found!');
});

// +++
app.use(function errorHandler(err, req, res, next) {
    res.status(err.status || 500);
    res.end(err.message);
});
// +++

var server = http.createServer(app);

server.listen(3000);

运行程序,浏览器访问http://localhost:3000后加任意无效的path,则会看到如下信息:

404

上面的代码里,errorHandler就是一个处理错误的middleware。它接受四个参数,第一个参数应该属于JS的Error类。当它之前有别的middleware在调用后面的middleware时传入一个Error对象(next(err)),errorHandler就会被调用。

更具体来说,在正常的“请求-应答回合”里,middleware是依照栈所定义的顺序依次调用的。处理错误的middleware也和其它的一样,需要添加到这个栈里。安装惯例,所有的处理错误的middleware都会处于栈的最后。如果没有错误发生,那么所有的处理错误的middleware都不会被调用,好像它们并不存在一样。如下图所示(这里假设某个处理错误的middleware处于正常middleware之间,而非最后):

没有错误的流程

而当某个middleware通过next(err)的方式通知Express应用有错误发生时,应用就会跳过它之后的所有正常middleware,直到第一个处理错误的middleware为止。这个处理错误的middleware在自己的任务最后,一般要么结束当前的“请求-应答回合”,要么仍然通过next(err)把错误传给下一个同类,让它进一步处理。

发生错误的流程

最后,特别强调下下面两点:

  1. Express中处理错误的middleware只会处理通过next(err)方式报出的错误,而不会处理throw出的错误
  2. 即使某个处理错误的middleware是整个栈的最后一个,在定义时也必须写四个参数(err, req, res, next),以免(err, req, res)(req, res, next)混淆

结语

中间件是Express框架的核心之一,本文算是对这个知识点的概述。Express的另一个核心routing,留待以后有时间再写。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容