理解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),能看到应该返回的信息。
上面的代码中的要点:
- 首先要导入
express
模块,并创建Express应用的对象 - 使用
use
方法来按顺序“登记”所定义的middleware。当收到客户端请求后,请求对象会依登记的顺序通过整个middleware栈(简单来说) - 注意在
logger
中手动调用了下一个middleware,但是在responser
中没有,这是因为我们知道它是最后一个middleware,所以可以省略参数与调用步骤
Middleware深入
通过上一节,我们了解了下面两个事实:
- Express应用实际上就是一连串middleware的组合
- 一般来讲,middleware就是一个方法,其参数为request对象、response对象和下一个middleware
实际上,上面对middleware的定义并不正确,因为(下面马上会说到)middleware也可能是一个接受四个参数的方法、甚至不是方法。
不妨这么定义:middleware是Express应用的逻辑单元;如果把从收到客户端请求到回复应答的过程称为“请求-应答回合”的话,那么一个middleware可能有如下四个功能:
- 执行不更改request和response对象的逻辑,比如之前的
logger
,它仅仅在命令行打印一段日志,并不处理request和response - 更改request和/或response对象
- 终结某个“请求-应答回合”,比如调用
response.end(...)
- 调用下一个middleware
换句话说,middleware就是能够传给app.use()
方法、负责一部分应用逻辑的东西。
按照其本质,middleware可以分为三类:
- application-level middleware(应用级别中间件)
- router-level middleware(分路级别中间件)
- error-handling middleware(处理错误中间件)
其中,application-level middleware就是常见的接受三个参数的方法((req, res, next) => {...}
);error-handling middleware则接受四个参数((req, res, next, error)
);而router-level middleware实际上并非方法,而是一个express.Router
对象。
按照来源,middleware又可以分为三类:
- 内置的
- 第三方的
- 应用开发者自写的
这个很好理解。开发Express应用可以理解为:自写middleware,选择使用内置、第三方middleware,并将其组织起来的过程。
下面我们来认识几个常用的第三方/内置middleware。
唯一的内置middleware模块
原生Node在处理客户端向服务器请求静态文件时,会非常麻烦。这一节则来看看Express应用是怎么处理这种情况的。
会到hello-express
文件夹,假设和app.js
文件所在同一位置有个名为public
的文件夹,下面有如下一个名为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);
上面的代码里值得注意的点为:
-
path
是Node的(而非Express的)一个内置模块,用来处理文件路径 -
__dirname
是当前正在运行的文件所在的地址 - 使用
path.join
而不是简单地__dirname + '/public'
是为了兼容Windows和Linux、Mac环境 -
express.static
就是一个serve-static
模块 -
express.static
出来接受静态文件的文件夹路径外,还可以接受一个JS Object来定义其行为;这里就不展开了,具体见前面给出的模块文档 - 一个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,则会看到如下信息:
上面的代码里,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)
把错误传给下一个同类,让它进一步处理。
最后,特别强调下下面两点:
- Express中处理错误的middleware只会处理通过
next(err)
方式报出的错误,而不会处理throw
出的错误 - 即使某个处理错误的middleware是整个栈的最后一个,在定义时也必须写四个参数
(err, req, res, next)
,以免(err, req, res)
和(req, res, next)
混淆
结语
中间件是Express框架的核心之一,本文算是对这个知识点的概述。Express的另一个核心routing,留待以后有时间再写。