中间件基础:理解 Connect 的核心机制

说实话,我刚接触 Connect 的时候,最困惑的就是「中间件」这三个字。听起来很高大上,对吧?其实说白了,中间件就是一个函数——一个在请求到达你真正业务逻辑之前,或者响应返回给客户端之前,帮你做点「手脚」的函数。

我个人习惯把中间件想象成流水线上的工人。每个工人只做一件事:检查、修改、记录,或者直接喊停。工人之间通过一条传送带连接,这条传送带就是 next() 函数。

中间件的定义:就是一个函数

在 Connect 里,中间件长这样:

function myMiddleware(req, res, next) {
  // 做点什么
  next();
}

三个参数:req(请求对象)、res(响应对象)、next(下一个中间件的回调)。你可以在函数里做任何事——解析请求体、记录日志、检查权限、修改响应头……做完之后,调用 next() 把控制权交给下一个。

核心要点:中间件要么调用 next() 继续传递,要么直接结束请求(比如 res.end())。二选一,没有中间状态。

我在项目中遇到过一位同事,他写了个中间件忘了调 next(),结果整个接口卡死了 5 分钟才超时。嗯,从那以后我们团队定了个规矩:每个中间件第一行就写 next() 的调用逻辑,再写其他代码。

next() 函数的作用:流水线的开关

next() 的作用就一个:告诉 Connect,「我干完了,下一个上」。但这里有几个细节,我建议你记牢:

  • 调用 next() 不带参数:正常流程,继续执行下一个中间件。
  • 调用 next(err) 带一个错误对象:跳过所有普通中间件,直接跳到错误处理中间件。
  • 不调用 next():请求被「挂起」,你必须自己调用 res.end() 或类似方法结束响应,否则客户端会一直等。

你想想看,这像不像 Promise 里的 resolve 和 reject?next() 就是 resolve,next(err) 就是 reject。设计思路其实是一样的。

小技巧:如果你想让某个中间件「既处理正常请求,又捕获错误」,可以在 try/catch 里调用 next(),在 catch 里调用 next(err)。这样错误就能统一流转到错误处理中间件。

中间件的执行顺序:注册顺序就是执行顺序

这是 Connect 最直观的规则——你 use() 的顺序,就是请求进来时执行的顺序。看个例子:

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

// 中间件 A
app.use(function(req, res, next) {
  console.log('A: 开始');
  next();
  console.log('A: 结束');
});

// 中间件 B
app.use(function(req, res, next) {
  console.log('B: 开始');
  next();
  console.log('B: 结束');
});

// 中间件 C
app.use(function(req, res, next) {
  console.log('C: 处理请求');
  res.end('Hello');
});

当请求进来时,控制台输出是:

A: 开始
B: 开始
C: 处理请求
B: 结束
A: 结束

为什么会这样?因为 next() 是异步的——它把控制权交给下一个中间件,等下一个执行完(包括它后面的代码),再回到当前中间件继续执行 next() 后面的代码。这其实就是个「洋葱模型」。

我曾经在调试一个性能问题时,发现日志顺序不对,排查了半天才发现是中间件顺序搞反了。后来我养成了一个习惯:把日志中间件放在最前面,错误处理中间件放在最后面,业务逻辑放中间。这个顺序几乎成了我所有项目的标配。

错误处理中间件:兜底的救火队

普通中间件有 3 个参数,错误处理中间件有 4 个:

function errorHandler(err, req, res, next) {
  console.error('发生错误:', err.message);
  res.statusCode = 500;
  res.end('服务器内部错误');
}

注意第一个参数是 err。Connect 识别中间件的方式很简单:看函数参数个数。参数个数为 4 的,就是错误处理中间件。

错误处理中间件的执行时机很特殊:

  • 只有当前面某个中间件调用了 next(err) 时,才会触发。
  • 它必须注册在所有普通中间件之后,否则不会生效。
  • 在错误处理中间件里,你也可以调用 next(err) 把错误继续往下传,但通常你会在这里结束请求。

注意:如果你没有注册任何错误处理中间件,Connect 会使用默认的错误处理器——直接返回 500 状态码和错误堆栈。这在生产环境是安全隐患。我建议你永远自己写一个错误处理中间件,至少把堆栈信息隐藏掉。

我曾经接手过一个项目,错误处理中间件写在最前面,结果所有错误都直接 500,连日志都没留下。排查问题时完全抓瞎。后来我把错误处理中间件移到末尾,并在里面加了结构化日志和错误分类,问题定位效率提升了好几倍。

一张图看懂中间件执行流程

下面这张 SVG 图展示了 Connect 中间件的完整执行流程,包括正常路径和错误路径:

Connect 中间件执行流程 Connect 中间件执行流程 HTTP 请求 中间件 1 日志 / 解析 / 鉴权 next() 中间件 2 业务逻辑 / 数据处理 next() 中间件 3 响应处理 / 格式化 HTTP 响应 next(err) 错误处理中间件 记录错误 / 返回 500 错误响应 500 说明: 正常路径(绿色)按注册顺序依次执行中间件,每个中间件调用 next() 传递控制权。 错误路径(红色)通过 next(err) 触发,直接跳转到错误处理中间件,跳过中间所有普通中间件。

三种中间件的对比

为了方便你快速查阅,我把普通中间件、错误处理中间件和终结中间件的区别整理成了表格:

类型 参数个数 触发条件 典型用途 是否必须调 next()
普通中间件 3 (req, res, next) 每次请求都会执行 日志、解析、鉴权、静态文件 通常需要,除非主动结束请求
错误处理中间件 4 (err, req, res, next) 前面有中间件调用了 next(err) 错误记录、统一错误响应 可选,通常直接结束请求
终结中间件 3 (req, res, next) 作为最后一个中间件 404 处理、默认响应 不调,直接 res.end()

避坑指南:我曾经犯过一个错误——在错误处理中间件里又调用了 next()(不带参数),结果请求又回到了普通中间件流程,导致死循环。记住:错误处理中间件里要么 next(err) 继续传递错误,要么直接结束请求,不要调无参的 next()。

小结一下

中间件是 Connect 的灵魂。你只要记住三件事:

  • 中间件就是个函数,next() 是流水线开关。
  • 注册顺序就是执行顺序,错误处理中间件放最后。
  • 4 个参数的是错误处理中间件,专门接 next(err) 抛过来的错误。