跳到主要内容

Express 中间件怎么理解:执行顺序、分类、错误处理与常见陷阱

面试速答(30 秒版)

  • Express 中间件本质上就是一组按顺序执行的函数,签名通常是 req, res, next
  • 它解决的是“请求到来后,哪些通用逻辑该按什么顺序被复用”,比如日志、鉴权、解析参数、跨域、限流。
  • 普通中间件决定请求如何继续向后流转;错误中间件专门处理异常,签名是 err, req, res, next
  • 面试里最关键的不是背 API,而是说清楚 3 件事:顺序、作用域、何时结束响应
  • 一句话总结:Express 中间件是把横切逻辑从业务 handler 里抽出来,再串成一条可组合的处理链。

一、先别背定义,先理解它为什么存在

如果没有中间件,你的每个路由都要自己写:

  • 打日志
  • 解析 body
  • 做权限校验
  • 校验参数
  • 统一处理错误

这样一来,代码会迅速变成复制粘贴。

Express 的思路是把这些公共逻辑抽出来,放到请求进入业务之前或之后统一处理。

可以把它想成:

这就是中间件最本质的价值:

  • 把通用逻辑和业务逻辑分层。

二、Express 中间件到底长什么样

最常见的普通中间件:

app.use((req, res, next) => {
req.startTime = Date.now();
next();
});

这段代码要说明 3 个点:

  1. reqres 是当前请求的上下文。
  2. next() 表示把控制权交给下一个匹配层。
  3. 如果不 next(),也不返回响应,请求就会挂住。

错误处理中间件长这样:

app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ message: "server error" });
});

注意它和普通中间件最大的区别是多了第一个参数 err


三、中间件有哪些分类

1. 按挂载方式分

全局中间件:

app.use(express.json());

特点:

  • 所有匹配路径的请求都可能经过它。

路由级中间件:

router.get("/profile", authMiddleware, handler);

特点:

  • 只对某个路由或某组路由生效。

2. 按职责分

常见类别:

  • 日志
  • body 解析
  • cookie / session
  • 鉴权
  • 参数校验
  • 限流
  • 静态资源
  • 错误处理

面试时建议补一句:

  • 中间件分类不是框架硬性规定,而是工程分层习惯。

四、Express 中间件为什么强调顺序

Express 的执行模型是线性的,先注册谁,谁就先匹配。

比如:

app.use(authMiddleware);
app.use("/admin", adminRouter);

此时访问 /admin/users,会先走 authMiddleware,再进入 adminRouter

但如果你把错误处理中间件放在前面:

app.use(errorHandler);
app.use("/api", router);

这个顺序通常就不对,因为错误处理中间件是拿来接住前面抛出的异常的,应该放在后面。

一个相对合理的顺序通常是:

  1. 请求标识、基础日志
  2. 限流、跨域、安全头
  3. body 解析、cookie 解析
  4. 鉴权
  5. 参数校验
  6. 业务路由
  7. 404 处理
  8. 错误处理中间件

五、路由和中间件是什么关系

很多人会把两者拆开讲,但在 Express 里,路由本质上也可以看成一种特殊处理层。

比如:

app.get("/users/:id", authMiddleware, getUserById);

这里本质发生了两件事:

  1. 先看 HTTP 方法和路径是否匹配。
  2. 匹配后依次执行挂在这条路由上的处理函数。

所以你可以把路由理解成:

  • 带路径和方法条件的中间件链入口。

六、next() 到底做了什么

这是面试常问点。

在 Express 里,next() 的含义可以理解成:

  • 当前层处理完了,继续交给后面的匹配层。

常见情况有 3 种:

  1. next():进入下一个普通层。
  2. next(err):跳过普通层,进入错误处理中间件。
  3. 不调用 next(),直接 res.send() / res.json():当前链路在这里结束。

最容易出问题的是既响应又 next(),比如:

app.use((req, res, next) => {
res.json({ ok: true });
next();
});

这可能导致后续逻辑继续执行,触发重复写响应。


七、错误处理中间件怎么讲才完整

1. 它的职责不是“打印报错”这么简单

更准确的职责是:

  • 统一转换错误结构
  • 隐藏内部细节
  • 记录日志
  • 按不同错误类型返回不同状态码

2. 最好有统一错误模型

比如:

class AppError extends Error {
constructor(status, message) {
super(message);
this.status = status;
}
}

配合错误处理中间件:

app.use((err, req, res, next) => {
const status = err.status || 500;
res.status(status).json({
message: err.message || "Internal Server Error",
});
});

这样接口行为更可控。


八、中间件设计的工程原则

写得好的中间件一般符合这些特点:

  • 单一职责,不要一个中间件里混日志、鉴权、参数校验。
  • 尽量无副作用,副作用要明确。
  • 尽量早失败,减少非法请求继续消耗资源。
  • 约定好往 req 上挂哪些字段,避免命名污染。
  • 错误直接往统一错误流收敛,不要每层都手写响应。

面试里很实用的一句是:

  • 中间件不是越多越好,而是越清晰、越可组合越好。

典型题与标准答法

1. Express 中间件是什么?

  • 是一组在请求处理过程中按顺序执行的函数。
  • 用来复用横切逻辑,比如日志、鉴权、解析和错误处理。

2. 为什么中间件顺序很重要?

  • 因为 Express 按注册顺序匹配和执行。
  • 顺序不对,可能导致鉴权失效、参数没解析、错误接不住。

3. next() 不调用会怎样?

  • 如果当前中间件也没有结束响应,请求会一直挂住。

4. 错误处理中间件和普通中间件区别是什么?

  • 错误处理中间件多一个 err 参数。
  • 它只在发生错误并进入错误流时执行。

常见追问

1. 中间件里能不能查数据库?

  • 能,但要谨慎。
  • 如果是每个请求都必须的数据,可以查;如果不是通用逻辑,最好留给业务层。

2. 中间件里往 req 上挂数据合理吗?

  • 合理,但要控制命名和边界。
  • 否则会变成隐式耦合。

3. 中间件和装饰器、AOP 有什么像的地方?

  • 都是在主业务逻辑外围织入横切逻辑。
  • 只是 Express 用的是请求链式处理模型。

易错点

  • 只会说“中间件就是一个函数”,说不出它解决什么问题。
  • 忘记错误处理中间件必须放在后面。
  • 在中间件里发送响应后还继续 next()
  • 一个中间件做太多事,导致难测难复用。
  • 把路由和中间件完全割裂看待。

速记要点

  • Express 中间件 = req, res, next 的处理链。
  • 核心是顺序、作用域、结束时机。
  • next() 是继续流转,next(err) 是进入错误流。
  • 路由可以看成带匹配条件的处理中间件层。
  • 好的中间件设计强调单一职责和统一错误收敛。