4. 中间件机制

Express 的核心并不是路由本身,而是“中间件流水线”。一个 HTTP 请求进入应用后,会依次经过一串中间件函数,每个函数都能读取请求对象 req、响应对象 res,并决定是继续交给下一个中间件,还是提前结束响应。理解这条流水线的顺序、next 的语义以及不同挂载级别的差异,是写出可维护 Express 服务的关键。

一、中间件的本质与 next 函数

中间件就是一个签名为 (req, res, next) 的函数(错误处理中间件多一个 err 参数)。它被注册到应用或路由器上,Express 在匹配到请求后按注册顺序调用它们。next 是一个由 Express 注入的回调,调用它表示“当前中间件处理完毕,请把控制权交给下一个匹配的中间件”。

需要区分三种典型行为:

  • 调用 next():正常放行,继续执行后续中间件;若后面没有匹配项,最终由 Express 返回 404。
  • 调用 next(err):跳过所有普通中间件,直接进入错误处理中间件。
  • 不调用 next 且已发送响应:请求在此终止,例如 res.send()、res.json() 之后不再调用 next,否则会触发“headers already sent”错误。

一个常见误区是:中间件里写了 res.send() 之后又调用 next(),导致后续中间件再次尝试写响应头。正确做法是发送响应后直接 return,或明确不再调用 next。

二、执行顺序:注册顺序即执行顺序

Express 严格按照 app.use() / app.get() 等注册的先后顺序匹配。先注册的中间件先执行,且只有调用 next() 才会进入下一个。下面这段示例展示了顺序对结果的影响:

const express = require('express');
const app = express();

app.use((req, res, next) => {
  console.log('A: 请求进入时间', Date.now());
  req.startAt = Date.now();
  next();
});

app.use('/api', (req, res, next) => {
  console.log('B: 命中 /api 前缀');
  next();
});

app.get('/api/users', (req, res) => {
  const cost = Date.now() - req.startAt;
  res.json({ ok: true, cost });
});

app.use((req, res) => {
  res.status(404).json({ error: 'Not Found' });
});

访问 /api/users 时,日志顺序固定为 A → B → 路由处理函数。若把 404 兜底中间件写在路由之前,则所有请求都会被它拦截,路由永远不会执行。因此“兜底 404”必须放在所有路由之后。

三、应用级中间件与路由级中间件

应用级中间件通过 app.use() 或 app.METHOD() 挂载,作用于整个应用。它常用于日志、请求体解析、CORS、鉴权等全局逻辑。挂载路径可以是前缀,例如 app.use('/admin', auth),表示只有以 /admin 开头的请求才经过该中间件。

路由级中间件通过 express.Router() 创建独立的路由器实例,再挂载到应用上。它把一组相关路由和中间件封装成模块,便于拆分代码。路由器内部的中间件只对进入该路由器的请求生效,不会污染全局。

// routes/users.js
const router = express.Router();

router.use((req, res, next) => {
  console.log('用户模块鉴权');
  next();
});

router.get('/', (req, res) => res.json([{ id: 1, name: 'Ada' }]));
router.get('/:id', (req, res) => res.json({ id: req.params.id }));

module.exports = router;

// app.js
app.use('/users', require('./routes/users'));

这里 router.use 是路由级中间件,只在 /users 前缀下生效。应用级与应用级、路由级与路由级之间都遵循“先注册先执行”的规则;当应用级中间件调用 next() 后,控制权会进入匹配的路由器,再在路由器内部继续按顺序执行。

四、错误处理中间件

错误处理中间件是参数为四个的中间件:(err, req, res, next)。Express 通过参数个数识别它,因此四个参数缺一不可,即使 next 没用到也要保留。它必须注册在所有普通中间件和路由之后,否则捕获不到前面抛出的错误。

app.get('/api/items/:id', (req, res, next) => {
  const id = Number(req.params.id);
  if (Number.isNaN(id)) {
    return next(new Error('id 必须是数字'));
  }
  res.json({ id });
});

app.use((err, req, res, next) => {
  console.error('捕获错误:', err.message);
  res.status(500).json({ error: err.message });
});

同步代码中抛出的异常会被 Express 自动捕获并转交错误处理中间件;但异步回调或 Promise 中抛出的错误不会自动传递,需要手动 next(err),或使用 async 包装并 catch。多个错误处理中间件可以串联,通过 next(err) 把错误继续传给下一个错误处理器,实现分层处理。

记忆口诀:普通中间件“三参数、调 next 放行”,错误中间件“四参数、放最后、接 err”。
Express 中间件执行流水线 HTTP 请求 应用级中间件 app.use(日志/解析) 路由级中间件 router.use(鉴权) 路由处理函数 app.get / router.get 发送响应 next() next() next() 错误处理中间件 (err, req, res, next) — 必须放最后 next(err) next(err) next(err) 500 错误响应 404 兜底中间件 app.use((req,res)=>...) 无匹配路由 正常 next() 放行 next(err) 错误传递 未匹配 → 404 兜底

五、顺序与挂载的实践要点

结合上面的流水线,可以总结出几条落地规则。第一,全局中间件(日志、express.json()、CORS)放在最前面,保证后续所有路由都能拿到解析后的请求体和统一上下文。第二,鉴权、限流等按前缀挂载的中间件放在对应路由之前,避免影响公开接口。第三,业务路由集中注册,最后再放 404 兜底和错误处理中间件。

当出现“接口返回 404 但路由明明写了”时,优先检查兜底中间件是否被提前注册;当出现“响应头已发送”报错时,检查是否在 res.send() 后仍调用了 next();当错误处理中间件不生效时,检查它是否只有三个参数,或是否被放在了路由之前。掌握这条流水线的顺序与 next 语义,就能把 Express 的中间件机制用得更稳、更清晰。