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 中间件)
  • 安全性:不会暴露目录结构,只返回存在的文件

我在项目中遇到过一个问题:静态文件更新后,浏览器还是显示旧版本。这是因为浏览器缓存了文件。

解决方案有两个:

  1. 设置 maxAge 为 0 或较短时间(开发环境用)
  2. 给文件名加版本号或哈希值(生产环境推荐)
// 开发环境:不缓存
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)放在最前面
  • 路由中间件放在中间
  • 错误处理中间件放在最后

这个顺序,我在项目里用了好几年,从来没出过问题。

一个小技巧:如果你不确定中间件的执行顺序,可以在每个中间件里打印一条日志。这样一眼就能看出执行顺序对不对。调试完再删掉就行。

好了,关于应用级中间件、路由级中间件、错误处理中间件和内置中间件,咱们就聊到这里。下一章,我会讲讲如何编写自己的中间件,以及一些高级用法。到时候见。