Express中间件模式

基本概念

中间件的本质就是一个函数,在收到请求和返回相应的过程中做一些我们想做的事情。

Express对中间件的作用的描述如下:

  • 执行任何代码
  • 修改请求和响应对象
  • 终结请求-响应循环
  • 调用堆栈中的下一个中间件

中间件可分为以下五类(原理相同,只是用法不同):

  • 应用级中间件
  • 路由级中间件
  • 错误处理中间件
  • 内置中间件
  • 第三方中间件

应用级中间件

应用级中间键绑定到 app 对象,使用 app.use 和 app.method (需要处理 http 请求的方法,例如get、put、post)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var app = express()

// 没有挂载路径的中间件,应用中的每个请求都会执行该中间件
app.use((req, res, next) => {
console.log('全局');
next();
});

// 路由和句柄函数(中间件系统),任何执行/user/:id的GET请求
app.get('/hero/:name',(req, res, next)=>{
console.log('Get hero', req.method);
});

// 挂载至/hero/:name的中间件,任何执行/hero/:name的请求都会执行它
app.use('/hero/:name',(req, res, next) => {
console.log('Request hero', req.method);
next();
});

路由级中间件

路由级中间件与应用级中间件相似,但绑定在 express 对象,使用router.use 和 router.method (需要处理 http 请求的方法,例如get、put、post)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var app = express();
var router = express.Router();

// 没有指定路径的中间件,通过该路由的所有请求都会执行该中间件
router.use((req, res, next) => {
console.log('全局路由');
next();
})


// 一个中间件,显示任何指向 /hero/:name 的HTTP请求的信息
router.use('/hero/:name', (req, res, next) => {
console.log('Request hero', req.originalUrl);
next();
});

// 处理/hero/:name的GET请求,渲染特定的页面
router.get('hero/:name', (req, res, next) => {
console.log(req.params.name);
res.render('hero');
})

// 将路由挂载至应用
app.use('/', router);

错误处理中间件

错误处理中间件,一定要声明四个参数,即使不需要next对象,也必须在参数中声明它,否者会被识别为一个常规中间件,无法处理错误信息。

1
2
3
4
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Oh no ....');
});

内置中间件

Express 从版本 4.x 开始,Express不再依赖 Content ,除了 express.static(处理静态资源), 以前内置的中间件现在已经全部单独作为模块安装使用。

1
2
3
4
5
6
7
8
const options = {
etag: false,
extensions: ['htm', 'html'],
index: false,
redirect: false,
};

app.use(express.static('public', options));

第三方中间件

使用第三方中间件,为 Express 应用增加更多的功能,可以在应用级中加载,也可以在路由级中加载。

1
2
3
4
5
6
7
8
9
10
// terminal
$ npm install cookie-parser

// express
const express = require('express');
const app = express();
const cookieParser = require('cookie-parser');

// 加载用于解析cookie的中间件
app.use(cookieParser());

原理

我们可以通过 Express 的源码进行深入了解。在源码中,与中间件有关的有三部分:

  • express.js继承application.js并对外暴露接口
  • application.js挂载了所有核心方法
  • router文件夹处理路由逻辑

以下是两端核心实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* Proxy `Router#use()` to add middleware to the app router.
* See Router#use() documentation for details.
*
* If the _fn_ parameter is an express app, then it will be
* mounted at the _route_ specified.
*
* @public
*/

app.use = function use(fn) {
var offset = 0;
var path = '/';

// 默认path 为 '/'
// app.use([fn])
//判断app.use传进来的是否是函数
if (typeof fn !== 'function') {
var arg = fn;

while (Array.isArray(arg) && arg.length !== 0) {
arg = arg[0];
}

// 第一个参数是路径
//取出第一个参数,将第一个参数赋值给path。
if (typeof arg !== 'function') {
offset = 1;
path = fn;
}
}
//slice.call(arguments,offset),通过slice转为数据,slice可以改变具有length的类数组。
//arguments是一个类数组对象。

//处理多种中间件使用方式。
// app.use(r1, r2);
// app.use('/', [r1, r2]);
// app.use(mw1, [mw2, r1, r2], subApp);

var fns = flatten(slice.call(arguments, offset));//[funtion]

//抛出错误
if (fns.length === 0) {
throw new TypeError('app.use() requires a middleware function')
}

//设置router
this.lazyrouter();
var router = this._router;

fns.forEach(function (fn) {
// 处理不是express的APP应用的情况,直接调用route.use。
if (!fn || !fn.handle || !fn.set) {
//path default to '/'
return router.use(path, fn);
}

debug('.use app under %s', path);
fn.mountpath = path;
fn.parent = this;
router.use(path, function mounted_app(req, res, next) {
var orig = req.app;
fn.handle(req, res, function (err) {
setPrototypeOf(req, orig.request)
setPrototypeOf(res, orig.response)
next(err);
});
});

// app mounted 触发emit
fn.emit('mount', this);
}, this);

return this;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var index = 0
function next(err) {
var layer = stack[index++];

if (!layer) {
defer(done, err);
return;
}

// route data
var path = parseUrl(req).pathname || '/';
var route = layer.route;

// skip this layer if the route doesn't match
if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
return next(err);
}

// call the layer handle
call(layer.handle, route, err, req, res, next);
}

通过源码,可以看得出中间件是线性的,next 过后继续寻找下一个中间件,这种模式直觉上也很好理解,中间件就是一系列数组,通过路由匹配来寻找相应路由的处理方法也就是中间件。事实上connect也是这么实现的。app.use就是往中间件数组中塞入新的中间件。中间件的执行则依靠私有方法app.handle进行处理。