12k
All articles

Node.js 中间件的工作原理

解析 Express 中间件的执行顺序,说明 next 如何控制调用链,以及 Express 5 在请求生命周期中处理异步错误的方式。

OpenReplay Team
OpenReplay Team
Node.js 中间件的工作原理

你可能在 Express 代码库中见过到处都是 app.use()。你知道中间件位于请求和响应之间。但当这个链条中出现问题时——请求挂起、错误消失或处理程序以错误的顺序触发——这个心智模型就崩溃了。

本文将解释 Node.js 中间件的实际工作原理:它是什么、控制流如何在链中传递,以及 Express 5 中发生了哪些变化。

核心要点

  • 中间件是一种框架模式,而非 Node.js 特性——Express 在核心 http 模块之上实现了它
  • 执行顺序很重要:中间件按注册顺序运行,错误处理程序必须放在最后
  • next() 函数控制流程;跳过它而不发送响应会导致请求挂起
  • Express 5 原生捕获来自异步中间件和路由处理程序的被拒绝的 Promise,无需手动包装器

中间件是框架模式,而非 Node.js 特性

Node.js 本身没有中间件概念。核心 http 模块只给你提供请求和响应对象——仅此而已。中间件是 Express 等框架在 Node.js 之上实现的一种模式。

Express 中间件遵循特定的结构:接收 reqresnext 的函数。框架维护这些函数的堆栈,并为每个传入请求按顺序调用它们。这种 Node.js 框架中的请求生命周期赋予了中间件强大的能力。

其他框架以不同方式实现类似的模式。Koa 使用”洋葱模型”,其中中间件包裹后续处理程序。Fastify 在特定生命周期点使用钩子。概念可以迁移,但语义不同。

请求-响应生命周期

当请求到达 Express 应用程序时,它进入一个管道:

  1. Express 将请求与已注册的中间件和路由进行匹配
  2. 每个中间件函数按注册顺序执行
  3. 控制通过 next() 向前传递,或在发送响应时停止
  4. 错误处理中间件捕获传递给 next(err) 的错误,以及在 Express 管理的中间件和路由处理程序中抛出或拒绝的错误

next() 函数是在链中移动控制的机制。调用它,Express 就会调用下一个匹配的中间件。跳过它而不发送响应,请求就会无限期挂起。

中间件顺序很重要

Express 中间件按你注册的顺序执行。这不仅仅是一个细节——它是 Node.js 中间件模式的基础。

app.use(parseBody)
app.use(authenticate)
app.use(authorize)
app.get('/data', handler)

在这里,parseBody 首先运行,使请求数据可供 authenticate 使用。交换它们的顺序,身份验证就会失败,因为请求体还没有被解析。

这种顺序也会影响错误处理。错误处理中间件必须放在它保护的路由之后。

短路响应

中间件可以通过发送响应而不调用 next() 来提前结束请求-响应周期。这就是身份验证中间件拒绝未授权请求的方式:

function requireAuth(req, res, next) {
  if (!req.user) {
    return res.status(401).json({ error: 'Unauthorized' })
  }
  next()
}

一旦执行了 res.send()res.json() 或类似方法,后续中间件就不会为该请求运行。这种短路是有意为之且很有用。

应用级、路由器级和路由级中间件

Express 中间件附加在不同的作用域:

应用级中间件为应用程序的每个请求运行。使用 app.use() 处理跨领域关注点,如日志记录或请求体解析。

路由器级中间件为匹配特定路由器的请求运行。这将中间件的作用域限定到路由组,而不影响整个应用程序。

路由级中间件仅为特定路由运行。将中间件函数直接传递给路由定义,以实现针对性行为,如验证。

这种区别在于作用域,而非能力。三者都使用相同的函数签名。

错误处理和 Express 5 中间件

Express 通过四参数签名识别错误处理中间件:(err, req, res, next)。当任何中间件调用 next(err) 或抛出错误时,Express 会跳过常规中间件并跳转到错误处理程序。

Express 5 改变了异步错误的工作方式。在 Express 4 中,被拒绝的 Promise 和异步异常需要手动处理或第三方包装器。Express 5 原生捕获来自异步函数的被拒绝的 Promise,并自动将它们路由到错误处理程序。

// Express 5: 不需要包装器
app.get('/data', async (req, res) => {
  const data = await fetchData() // 拒绝会触发错误中间件
  res.json(data)
})

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

错误处理中间件仍然需要最后注册,在路由和其他中间件之后。

结论

中间件是一种框架级模式,用于通过函数链处理请求。理解执行顺序、next() 的作用以及错误处理程序的放置位置,可以消除大多数中间件调试难题。

Express 5 的原生异步错误处理消除了一个常见痛点,但基本原理保持不变:按正确顺序注册中间件,调用 next() 或发送响应,并将错误处理程序放在最后。

常见问题

如果我忘记在中间件中调用 next() 会发生什么?

如果你忘记调用 next() 并且不发送响应,请求将无限期挂起。Express 会等待 next() 被调用以继续链,或等待响应被发送到客户端。始终确保你的中间件要么调用 next() 向前传递控制,要么发送响应以结束周期。

我可以在 Express 4 中间件中使用 async/await 吗?

可以,但你必须手动处理错误。Express 4 不会自动捕获被拒绝的 Promise,因此未处理的拒绝不会触发你的错误处理中间件。将异步代码包装在 try-catch 块中,或使用将错误转发到 next() 的包装函数。Express 5 通过原生捕获拒绝消除了这一要求。

为什么我的错误处理中间件从未被调用?

错误处理中间件需要恰好四个参数:err、req、res 和 next。如果省略任何参数,Express 会将其视为常规中间件并在错误处理期间跳过它。还要验证你的错误处理程序是否在应用程序中的所有路由和其他中间件之后注册。

app.use() 和 router.use() 有什么区别?

两者都使用相同的函数签名注册中间件,但它们的作用域不同。app.use() 在应用程序级别附加中间件,为所有请求运行。router.use() 将中间件附加到特定的路由器实例,仅为匹配该路由器挂载路径的请求运行。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.