先记住这个答案
Express 把每个请求交给一条按代码注册顺序排列的中间件队列,逐个检查挂载路径是否匹配当前请求路径的前缀,匹配且调用 next() 才轮到下一个。路由处理函数本质上也是这条链上的中间件。顺序写错的典型后果是:把错误处理中间件(四参数函数)放在路由之前,它只会在链上被跳过,路由里抛错后它收不到 next(err),最终错误落到内置默认处理器或进程崩溃。判断口诀:解析类中间件在前,路由居中,404 与错误处理垫底。
- 执行顺序由注册顺序加路径前缀匹配决定
- 错误处理中间件必须注册在所有其他中间件之后
- 不发响应必须调 next(),否则请求挂起
注册顺序与路径前缀如何共同决定执行链
Express 内部维护一条扁平的中间件层数组,每次调用 app.use、app.get 等都按调用先后往数组里压入一层。请求到来时从头遍历:先检查该层的挂载路径是否为请求路径的前缀(不带路径的 app.use 等价于挂载在 /,匹配一切),匹配则执行该层函数,函数里调用 next() 才继续向后找下一层匹配项。
因此最终执行链是两个条件的交集:写在前面且路径匹配。一个挂在 /admin 的鉴权中间件无论写多靠前,对 /api/list 都不会执行;同样,即使路径匹配,写在路由之后的普通中间件也轮不到,因为路由通常直接 res.send 结束了响应周期。路由处理函数在机制上就是链上的一层,只是按 HTTP 方法再过滤一次。
把错误处理中间件写在路由之前导致线上 500 兜底失效
假设一个项目约定所有业务错误统一返回 JSON:{ code, message },开发者把四参数的错误处理中间件写在文件顶部、app.get('/orders') 等路由之前。在预期场景中,当订单接口查询数据库超时抛错时,客户端不会收到约定的 JSON,而是 Express 默认处理器返回的 HTML 堆栈页,监控也无法按 code 字段聚合告警。
原因是错误处理中间件必须位于错误产生点之后:路由里 throw 或 next(err) 时,Express 从当前位置向后查找第一个四参数层,写在路由之前的层已经在普通流程里被跳过(普通流程中四参数层不参与匹配),永远不会被错误链重新找到。修复方法是把它移到所有路由和 404 中间件之后,顺序变为解析器、业务路由、404、错误处理。
顺序规则在哪些条件下容易误判
容易误判的第一处是 next('route') 与 next() 的区别:在 app.get 的多个回调里,next() 只是进入同一路由的下一个回调,并非进入下一个中间件层;第二处是 Router,router.use 的相对路径叠加上 app.use('/api', router) 的前缀,执行顺序按挂载点展开,阅读单个文件容易看错全局顺序。
对应处理:排查顺序问题时,以应用入口文件为准,把所有 app.use 与路由注册按行号列出来,再逐条核对前缀;用日志中间件临时打印 req.method 与 req.originalUrl 验证实际链路。代价是模块化项目中链路被拆散到多个 Router 文件,需要约定每个 Router 内部自包含其私有中间件,避免跨文件的隐式顺序依赖。
容易答错的地方
- 错误处理中间件放前面也能兜底
- 错误。普通请求流程中四参数层被跳过,错误发生时 Express 只从出错位置向后找四参数层,写在路由之前的错误处理器不会被调用,错误会落到内置默认处理器。
- app.use 不带路径就只执行一次
- 不带路径的
app.use挂载在/,对每个进入应用的请求都按顺序执行一次,不是启动时执行一次。若其中既不发响应也不调next(),后续所有请求都会挂起。
面试官还会怎么问?
路径匹配是精确匹配还是前缀匹配?
app.use 是前缀匹配,app.use('/api') 会匹配 /api、/api/users;app.get 等路由方法是整路径匹配。判断某层是否进入执行链时先看这个区别。
路由之后还能有中间件被执行吗?
可以,但前提是路由处理函数不调响应方法而调 next(),此时控制权继续向后走,常用于统一记录响应日志或后置处理;一旦发送了响应,再调 next() 容易引发重复发送错误。
多个 app.use 挂同一路径,顺序怎么定?
严格按代码书写顺序逐个执行,前一个调 next() 才轮到后一个,形成挂载点上的子栈。把鉴权写在校验之后,就会出现未鉴权请求先触发校验逻辑的问题。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。