转载

每日一博 | 理解 Express 的 middleware

Middleware初识

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

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

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

每日一博 | 理解 Express 的 middleware

那么middleware到底是什么呢?简单来说,在代码中,middleware实际上就是一个方法,类似于前面的

requestHandler

方法,它也接受代表请求和应答的两个对象(由http创建,由Express增强)。但此外,它还接受一个参数,这个参数代表了栈中下一个应该执行的middleware。即是说,middleware的代码一般都长成下面这个样子:

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

注意最后需要手动调用下一个middleware(

next()

),否则请求的处理过程会被挂起,直到超时。

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

每日一博 | 理解 Express 的 middleware

光讲理论不太好理解,下面来看看实际上如何用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),能看到与之前相同的结果。

上面的代码中的要点:

  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或者终结“请求-应答回合”时)

换句话说,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

对象。最后一个留到Express的routing章节细讲,后文里会细讲下处理错误的中间件,不过接下来还得提下middleware的另一种分类。

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

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

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

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

唯一的内置middleware模块

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

会到

hello-express

文件夹,假设和

app.js

文件所在同一位置有个名为

public

的文件夹,下面有如下一个名为

marigold.jpg

的图像文件:

每日一博 | 理解 Express 的 middleware

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

这就要用到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); ```

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

  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

,则可以看到服务器成功地回应了我们所请求的文件:

每日一博 | 理解 Express 的 middleware

第三方middleware模块:morgan

“重新发明轮子”是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'
模式,详细信息请见 其文档

第三方middleware模块:body-parser

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模块还包括:

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

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

处理错误的middleware

处理错误的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,则会看到如下信息:

每日一博 | 理解 Express 的 middleware

上面的代码里,

errorHandler

就是一个处理错误的middleware。它接受四个参数,第一个参数应该属于JS的

Error

类。当它之前有别的middleware在调用后面的middleware时传入一个

Error

对象(

next(err)
errorHandler

就会被调用。

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

每日一博 | 理解 Express 的 middleware

而当某个middleware通过

next(err)

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

next(err)

把错误传给下一个同类,让它进一步处理。

每日一博 | 理解 Express 的 middleware

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

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

结语

中间件是Express框架的核心之一,本文算是对这个知识点的概述。下一篇我们会介绍Express的另一个核心:routing。敬请期待。

原文  http://my.oschina.net/qiaotoubao/blog/735675
正文到此结束
Loading...