中间件基础:理解 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 中间件的完整执行流程,包括正常路径和错误路径:
三种中间件的对比
为了方便你快速查阅,我把普通中间件、错误处理中间件和终结中间件的区别整理成了表格:
| 类型 | 参数个数 | 触发条件 | 典型用途 | 是否必须调 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)抛过来的错误。