简介
从概念上讲,中间件是一种功能的封装方式,具体来说就是封装在程序中处理HTTP请求的功能。
中间件是在管道中执行的,在Express程序中,通过调用app.use向管道中插入中间件。(在Express 4.0中,中间件和路由处理器是按它们的连入顺序调用的)
在管道的最后放一个“捕获一切”请求的处理器是常见的做法,由它来处理跟前面其他所有路由都不匹配的请求。这个中间件一般会返回状态码404(未找到)。
如果不调用next(),请求就在那个中间件中终止了。
中间件和路由处理器
路由处理器(app.get、app.post等,经常被统称为app.VERB)可以被看作只处理特定HTTP谓词(GET、POST等)的中间件; 也可以将中间件看作可以处理全部HTTP谓词的路由处理器(基本上等同于app.all,可以处理任何HTTP谓词;
路由处理器的第一个参数必须是路径;中间件也可以将路径作为第一个参数,但它是可选的(如果忽略这个参数,它会匹配所有路径)。
-
路由处理器和中间件的参数中都有回调函数,这个函数有2个、3个或4个参数;
- 如果有2个或3个参数,头两个参数是请求和响应对象,第三个参数是next函数。
- 如果有4个参数,它就变成了错误处理中间件,第一个参数变成了错误对象,然后依次是请求、响应和next对象。
-
如果不调用next(),管道就会被终止,也不会再有处理器或中间件做后续处理。
- 如果不调用next(),则应该发送一个响应到客户端(res.send、res.json、res.render等);
- 如果不这样做,客户端会被挂起并最终导致超时。
-
如果调用了next(),一般不宜再发送响应到客户端。
- 如果发送了,管道中后续的中间件或路由处理器还会执行,但它们发送的任何响应都会被忽略。
app.use(function(req, res, next){
console.log('processing request for "' + req.url + '"....');
next();
});
app.use(function(req, res, next){
console.log('terminating request');
res.send('thanks for playing!');
// 注意,我们没有调用next()......这样请求处理就终止了;
//如果忽略了`res.send`,则不会有响应返回到客户端,最终会导致客户端超时。
});
//最后一个中间件永远也不会执行
app.use(function(req, res, next){
console.log('whoops, i\'ll never get called!');
});
var app = require('express')();
app.use(function(req, res, next){
console.log('\n\nALLWAYS');
next();
});
app.get('/a', function(req, res){
console.log('/a: 路由终止');
res.send('a');
});
app.get('/a', function(req, res){
console.log('/a: 永远不会调用');
});
app.get('/b', function(req, res, next){
console.log('/b: 路由未终止');
next();
});
app.use(function(req, res, next){
console.log('SOMETIMES');
next();
});
app.get('/b', function(req, res, next){
console.log('/b (part 2): 抛出错误' );
throw new Error('b 失败');
});
app.use('/b', function(err, req, res, next){
console.log('/b 检测到错误并传递');
next(err);
});
app.get('/c', function(err, req){
console.log('/c: 抛出错误');
throw new Error('c 失败');
});
app.use('/c', function(err, req, res, next){
console.log('/c: 检测到错误但不传递');
next();
});
app.use(function(err, req, res, next){
console.log('检测到未处理的错误: ' + err.message);
res.send('500 - 服务器错误');
});
app.use(function(req, res){
console.log('未处理的路由');
res.send('404 - 未找到');
});
app.listen(3000, function(){
console.log('监听端口3000');
});
要特别注意请求/b和请求/c的差异,在这两个实例中都有一个错误,但一个结果是404,另一个是500。
中间件必须是一个函数
模块可以输出一个函数,而这个函数又可以直接用作中间件。
//lib/tourRequiresWaiver.js模块
module.exports = function(req,res,next){
var cart = req.session.cart;
if(!cart) return next();
if(cart.some(function(item){ return item.product.requiresWaiver; })){
if(!cart.warnings) cart.warnings = [];
cart.warnings.push('One or more of your selected tours' +
'requires a waiver.');
}
next();
}
//引入这个中间件:
app.use(require('./lib/requiresWaiver.js'));
//不过更常见的做法是输出一个以中间件为属性的对象
module.exports = {
checkWaivers: function(req, res, next){
var cart = req.session.cart;
if(!cart) return next();
if(cart.some(function(i){ return i.product.requiresWaiver; })){
if(!cart.warnings) cart.warnings = [];
cart.warnings.push('One or more of your selected ' +
'tours requires a waiver.');
}
next();
},
checkGuestCounts: function(req, res, next){
var cart = req.session.cart;
if(!cart) return next();
if(cart.some(function(item){ return item.guests >
item.product.maximumGuests; })){
if(!cart.errors) cart.errors = [];
cart.errors.push('One or more of your selected tours ' +
'cannot accommodate the number of guests you ' +
'have selected.');
}
next();
}
}
//像以下这样连入中间件
var cartValidation = require('./lib/cartValidation.js');
app.use(cartValidation.checkWaivers);
app.use(cartValidation.checkGuestCounts);
在前面的例子中,我们的中间件会用语句return next()提前终止。Express不期望中间件返回值(并且它不会用返回值做任何事情),所以这只是缩短了的next(); return;。
常用中间件
Express 4.0之后,唯一保留在Express中的中间件只剩下static
basicAuth: 提供基本的访问授权。basic-auth只提供最基本的安全,并且只能通过HTTPS使用basic-auth(否则用户名和密码是通过明文传输的)。只有在需要又快又容易的东西,并且在使用HTTPS时,才应该用basic-auth。
body-parser: 只连入json和urlencoded的便利中间件。
json: 解析JSON编码的请求体。
urlencoded: 解析互联网媒体类型为
application/x-www-form-urlencoded
的请求体。这是处理表单和AJAX请求最常用的方式;multipart(已废弃): 解析互联网媒体类型为
multipart/form-data的请求体
; 应该用Busboy或Formidable代替它。compress: 用gzip压缩响应数据。特别使用在那些网络比较慢或者用手机上网的情况; 它应该在任何可能会发送响应的中间件之前被尽早连入。唯一应该出现在compress之前的中间件只有debugging或logging(它们不发送响应)。
cookie-parser: 提供对cookie的支持;
app.use(require(cookie-parser)(秘钥放在这里)
;cookie-session: 提供cookie存储的会话支持。一般不推荐使用这种存储方式的会话;一定要把它放在
cookie-parser
后面连入。express-session: 提供会话ID (存在cookie里)的会话支持。默认存在内存里,不适用于生产环境,并且可以配置为使用数据库存储。
csurf: 防范跨域请求伪造(CSRF)攻击。因为它要使用会话,所以必须放在
express-session
中间件后面。它目前等同于connect.csrf
中间件。可惜简单连入这个中间件并不能神奇地防范CSRF攻击;directory: 提供静态文件的目录清单支持。如果不需要目录清单,则无需引入这个中间件。
errorhandler: 为客户端提供栈追踪和错误消息;建议不要在生产环境中连入它,因为它会暴露实现细节,可能引发安全或隐私问题。
static-favicon: 提供favicon(出现在浏览器标题栏上的图标)。这个中间件不是必需的,可以简单地在static目录下放一个favicon.ico,但这个中间件能提升性能。如果要使用它,应该尽可能地往中间件栈的上面放。
morgan: 提供自动日志记录支持:所有请求都会被记录。
method-override: 提供对
x-http-method-override
请求头的支持,允许浏览器“假装”使用除GET
和POST
之外的HTTP
方法。这对调试有帮助。只在编写API时才需要。query: 解析查询字符串,并将其变成请求对象上的
query
属性。这个中间件是由Express隐含连入的,所以不要自己连入它。response-time: 向响应中添加
X-Response-Time
头,提供以毫秒为单位的响应时间。一般在做性能调优时才需要这个中间件。static: 提供对静态(public)文件的支持; 这个中间件可以连入多次,并可指定不同的目录。
vhost: 虚拟主机(vhost),这个术语是从Apache借来的,它可使子域名在Express中更容易管理。