Express有两个核心概念:middleware和routing,也是使得Express应用模块化、组织清晰、可维护性高的关键。本篇先来讲讲middleware。
Middleware,可以翻译成“中间件”,其本质就是一个处理request的方法,但是单个middleware并不完成所有的逻辑。打个比方,原生Node(意思是“用Node的http模块创建的网络应用”,见本系列文章前一篇)中处理request的逻辑是铁板一块,而Express则将其重组成一长根链条,每一环都是一个middleware、负责处理好一小部分(do one thing well),依次来完成整个处理逻辑。
下图是原生Node和Express处理客户端请求的逻辑的对比。
那么middleware到底是什么呢?简单来说,在代码中,middleware实际上就是一个方法,类似于前面的
requestHandler
方法,它也接受代表请求和应答的两个对象(由http创建,由Express增强)。但此外,它还接受一个参数,这个参数代表了栈中下一个应该执行的middleware。即是说,middleware的代码一般都长成下面这个样子:
javascript function aMiddleware(request, response, next) { // ... next(); }
注意最后需要手动调用下一个middleware(
next()
),否则请求的处理过程会被挂起,直到超时。
所以,对Express应用来说,整个图景是这个样子:
光讲理论不太好理解,下面来看看实际上如何用Express来实现上一篇中的简单应用。
首先新建一个应用文件夹:
bash $ mkdir hello-express $ cd hello-express
安装Express:
bash $ npm install 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的定义并不正确,因为(下面马上会说到)middleware也可能是一个接受四个参数的方法、甚至不是方法。
不妨这么定义:middleware是Express应用的逻辑单元;如果把从收到客户端请求到回复应答的过程称为“请求-应答回合”的话,那么一个middleware可能有如下四个功能:
logger,它仅仅在命令行打印一段日志,并不处理request和response
response.end(...)
换句话说,middleware就是能够传给
app.use()
方法、负责一部分应用逻辑的东西。
按照其本质,middleware可以分为三类:
其中,application-level middleware就是常见的接受三个参数的方法(
(req, res, next) => {...}
);error-handling middleware则接受四个参数(
(req, res, next, error)
);而router-level middleware实际上并非方法,而是一个
express.Router
对象。最后一个留到Express的routing章节细讲,后文里会细讲下处理错误的中间件,不过接下来还得提下middleware的另一种分类。
按照来源,middleware又可以分为三类:
这个很好理解。开发Express应用可以理解为:自写middleware,选择使用内置、第三方middleware,并将其组织起来的过程。
下面我们来认识几个常用的第三方/内置middleware。
原生Node在处理客户端向服务器请求静态文件时,会非常麻烦。这一节则来看看Express应用是怎么处理这种情况的。
会到
hello-express
文件夹,假设和
app.js
文件所在同一位置有个名为
public
的文件夹,下面有如下一个名为
marigold.jpg
的图像文件:
那么如何处理客户端对这幅图的处理呢?或者说,如何使得服务器能够向客服端“服务”这个文件呢?
这就要用到Express V4.x 唯一 的内置middleware模块了:
serve-static。因为它是内置模块,所以不用下载和导入任何东西。具体使用的时候,只需告诉该模块这些静态文件所在的文件夹位置,它就会返回一个middleware好让开发者将其加入到应用的middleware栈里。
打开
app.js
文件,将其内容更改为:
```javascript 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); ```
上面的代码里值得注意的点为:
__dirname是当前正在运行的文件所在的地址
path.join而不是简单地
__dirname + '/public'是为了兼容Windows和Linux、Mac环境
express.static就是一个
serve-static模块
express.static出来接受静态文件的文件夹路径外,还可以接受一个JS Object来定义其行为;这里就不展开了,具体见前面给出的模块文档
保存文件,运行程序
node app.js
,浏览器访问
http://localhost:3000/marigold.jpg
,则可以看到服务器成功地回应了我们所请求的文件:
“重新发明轮子”是IT圈的大忌。在写middleware之前,最好看看是不是已经有人帮我们实现了想要的功能。
比如前面的log功能,就可以直接用第三方的morgan模块。(实际上,这个模块也是Express小组维护的,是从以前版本的Express分离出去的。)
回到
hello-express
文件夹下,安装morgan模块:
bash $ npm install morgan
然后更改
app.js
文件如下:
```javascript 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'模式,详细信息请见 其文档
body-parser 是Express最重要的第三方middleware模块之一。它将客服端发来的HTTP请求解析成JS/Node对象。这一节我们来说说它的具体用法。
跟morgan一样,首先我们要安装body-parser:
bash $ npm install body-parser
并在代码中导入:
javascript var bodyParser = require('body-parser');
在Express应用中,经常见到这样的使用body-parser模块的方法:
javascript app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true }));
bodyParser.json()
会返回一个middleware。当来自客户端的HTTP请求的MIME类型为
application/json
时(也就是含有头字段
Cotent-Type: application/json
时),这个middleware就会试着把请求的body部分(最后的那一部分)解析成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模块还包括:
request.cookie
这里都不做细讲了,有兴趣的同学可以自行搜索学习。
处理错误的middleware和应用级别的middleware就只有一个差别,那就是它接受四个参数,其签名可以写为
(err, req, res, next)=>void
边做边学,我们来看看实际上怎么应用。假设,在刚刚开始写一个网络应用的时候,还没有专门的404页面,而是想把404信息当成错误来处理,则可以像下面这样处理。
回到
hello-express
文件夹,修改
app.js
如下:
```javascript 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)
把错误传给下一个同类,让它进一步处理。
最后,特别强调下下面两点:
next(err)方式报出的错误,而不会处理
throw出的错误
(err, req, res, next),以免混淆
中间件是Express框架的核心之一,本文算是对这个知识点的概述。下一篇我们会介绍Express的另一个核心:routing。敬请期待。