Nodejs程序员后端开发大本营

理解Express的middleware

2018-02-04  本文已影响24人  桥头堡2015

什么是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可能有如下四个功能:

换句话说,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模块还包括:

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

处理错误的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,留待以后有时间再写。

上一篇下一篇

猜你喜欢

热点阅读