2. 应用级中间件:app.use()详解、路由级中间件、错误处理中间件、内置中间件(express.static)
好,咱们今天来聊聊 Express 中间件的几个核心类型。说实话,中间件这个概念,刚接触 Node.js 的朋友很容易搞混。我当年也是,看了半天文档,总觉得「不就是个函数吗?怎么还分这么多种?」
嗯,等你真正在项目里跑起来,就会发现——分清楚它们,太重要了。
2.1 应用级中间件:app.use() 详解
先说说最常用的 app.use()。这是 Express 应用实例上的方法,用来挂载中间件函数。
它的签名很简单:
app.use([path,] callback [, callback...])
说白了,就是「当请求进来时,如果路径匹配,就执行这个函数」。如果不传 path,默认匹配所有路径。
我在项目中遇到过这样一个场景:刚开始写接口时,所有路由都直接写在 app.get() 里。后来接口多了,发现日志、鉴权、参数校验这些逻辑散落在各处。改起来特别痛苦。
后来我统一用 app.use() 挂载全局中间件,代码瞬间清爽了。
核心要点:
app.use()可以挂载任意中间件函数- 不传路径时,匹配所有请求
- 传路径时,只匹配以该路径开头的请求
- 中间件按注册顺序执行
来看个例子:
const express = require('express');
const app = express();
// 全局中间件:记录请求日志
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
// 路径特定的中间件:只对 /api 开头的请求生效
app.use('/api', (req, res, next) => {
console.log('API 请求来了');
next();
});
app.get('/', (req, res) => {
res.send('首页');
});
app.get('/api/users', (req, res) => {
res.json({ users: ['Alice', 'Bob'] });
});
app.listen(3000);
你想想看,如果不用 app.use(),每个路由都要手动加日志逻辑,那得多累?
我的习惯:把全局中间件放在路由定义之前。这样能确保每个请求都经过它们。顺序很重要,别搞反了。
2.2 路由级中间件
路由级中间件,其实跟应用级中间件用法几乎一样。唯一的区别是:它绑定在 express.Router() 实例上。
为什么要用路由级中间件?
我举个例子。假设你的项目有用户模块、商品模块、订单模块。每个模块都有自己的一套中间件逻辑。如果全写在 app.js 里,那文件会膨胀到没法看。
路由级中间件就是用来做模块化拆分的。
const express = require('express');
const router = express.Router();
// 路由级中间件:验证用户是否登录
router.use((req, res, next) => {
if (!req.session || !req.session.userId) {
return res.status(401).json({ error: '未登录' });
}
next();
});
// 路由级中间件:记录用户操作日志
router.use((req, res, next) => {
console.log(`用户 ${req.session.userId} 执行了 ${req.method} ${req.url}`);
next();
});
router.get('/profile', (req, res) => {
res.json({ userId: req.session.userId, name: '张三' });
});
router.post('/update', (req, res) => {
// 更新用户信息
res.json({ success: true });
});
module.exports = router;
然后在主应用里挂载:
const userRouter = require('./routes/user');
app.use('/user', userRouter);
这样一来,用户模块的中间件逻辑都封装在 userRouter 里。主文件只负责挂载,清爽得很。
注意:路由级中间件中的 next() 如果传了参数(比如 next('route')),会跳过当前路由,执行下一个匹配的路由。这个特性在复杂场景下很有用,但也容易踩坑。我曾经因为传错参数,导致请求莫名其妙地跳过了所有后续中间件,排查了半天。
2.3 错误处理中间件
错误处理中间件,是 Express 里一个特殊的存在。它跟普通中间件的区别在于——它有 四个参数。
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('服务器出错了!');
});
嗯,这里要注意:Express 识别错误处理中间件的方式,就是看参数个数。四个参数,一个都不能少。少一个,Express 就当它是普通中间件了。
为什么会这样?
因为 JavaScript 函数可以任意传参。Express 内部通过检查 fn.length 来判断中间件类型。四个参数就是错误处理,三个或更少就是普通中间件。
我在项目中遇到过这样一个坑:写错误处理中间件时,不小心把 err 参数漏掉了,结果错误没有被捕获,直接导致进程崩溃。嗯,从那以后我写错误处理中间件都会数一遍参数。
最佳实践:
- 错误处理中间件要放在所有路由和中间件之后
- 在路由中通过
next(err)把错误传递过来 - 不要在生产环境暴露详细的错误堆栈
来看一个完整的例子:
const express = require('express');
const app = express();
// 普通中间件
app.use((req, res, next) => {
console.log('普通中间件');
next();
});
// 路由
app.get('/', (req, res) => {
res.send('首页');
});
app.get('/error', (req, res, next) => {
// 模拟一个错误
const err = new Error('出错了!');
err.status = 400;
next(err);
});
// 错误处理中间件(必须放在最后)
app.use((err, req, res, next) => {
const status = err.status || 500;
const message = err.message || '服务器内部错误';
console.error(`[错误] ${status} - ${message}`);
res.status(status).json({
error: message,
// 生产环境不要暴露 stack
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
});
app.listen(3000);
我的建议:写一个统一的错误处理中间件,把错误分类处理。比如 400 类错误返回用户提示,500 类错误记录日志并返回通用信息。这样前端和后端都省心。
2.4 内置中间件:express.static
Express 自带了一些中间件,其中最常用的就是 express.static。它用来托管静态文件——比如 HTML、CSS、JS、图片等。
说白了,就是让用户可以直接通过 URL 访问服务器上的文件。
const express = require('express');
const app = express();
// 托管 public 目录下的静态文件
app.use(express.static('public'));
// 也可以指定虚拟路径前缀
app.use('/static', express.static('public'));
app.listen(3000);
假设 public 目录下有个 images/logo.png,那么用户访问 http://localhost:3000/images/logo.png 就能看到图片。
如果用了虚拟路径前缀,那就是 http://localhost:3000/static/images/logo.png。
express.static 的特点:
- 自动处理文件 MIME 类型
- 支持缓存控制(通过
maxAge选项) - 支持文件压缩(配合
compression中间件) - 安全性:不会暴露目录结构,只返回存在的文件
我在项目中遇到过一个问题:静态文件更新后,浏览器还是显示旧版本。这是因为浏览器缓存了文件。
解决方案有两个:
- 设置
maxAge为 0 或较短时间(开发环境用) - 给文件名加版本号或哈希值(生产环境推荐)
// 开发环境:不缓存
app.use(express.static('public', { maxAge: 0 }));
// 生产环境:缓存 1 天
app.use(express.static('public', { maxAge: 86400000 }));
注意:不要把 express.static 指向项目根目录。否则用户可能下载到你的 package.json、.env 等敏感文件。我曾经见过有人把整个项目目录都暴露出去,结果数据库密码被扒了个精光。
2.5 中间件的执行顺序
最后,咱们聊聊中间件的执行顺序。这是很多新手容易搞混的地方。
Express 中间件的执行顺序,遵循一个简单的规则:先进先出。也就是按照注册顺序,从上到下依次执行。
但有几个特殊情况:
| 情况 | 行为 |
|---|---|
中间件调用了 next() |
继续执行下一个中间件 |
中间件调用了 next('route') |
跳过当前路由,执行下一个匹配的路由 |
中间件调用了 res.send() 等响应方法 |
终止请求-响应循环,不再执行后续中间件 |
中间件调用了 next(err) |
跳转到错误处理中间件 |
你想想看,如果中间件顺序搞错了,比如把错误处理中间件放在路由前面,那错误就永远不会被捕获到。
嗯,这就是为什么我建议:
- 全局中间件(日志、鉴权、解析 body)放在最前面
- 路由中间件放在中间
- 错误处理中间件放在最后
这个顺序,我在项目里用了好几年,从来没出过问题。
一个小技巧:如果你不确定中间件的执行顺序,可以在每个中间件里打印一条日志。这样一眼就能看出执行顺序对不对。调试完再删掉就行。
好了,关于应用级中间件、路由级中间件、错误处理中间件和内置中间件,咱们就聊到这里。下一章,我会讲讲如何编写自己的中间件,以及一些高级用法。到时候见。