先记住这个答案
多个处理函数既可以作为app.get('/path', fn1, fn2)的独立参数,也可以写成数组app.get('/path', [fn1, fn2]),还能混合使用。Express把同一路由上的所有函数按从左到右的顺序依次执行。每个函数必须接收next参数,并在需要继续时调用next();若函数调用res.send()等响应方法,则后续函数不再执行,请求处理结束。
- 多个回调按声明顺序串联
- next()是进入下一回调的唯一入口
- 发响应后后续回调不会被调用
路由级回调链的流动规则
Express在处理app.get(path, fn1, fn2)时,会把所有传入的处理器函数收集到一个数组里,并把它们当作一条顺序链。当请求匹配该路径时,Express先调用链上的第一个函数,传入(req,res,next)。这个next是Express生成的内部函数,它的作用是把控制权交给当前链条中的下一个函数。如果第一个函数不调用next(),也没有结束响应,那么请求就停在它这里,后面的函数永远不会被触发。
如果某个函数调用了res.send()、res.json()、res.end()等,Express会认为响应已经发出,即使它之后又写了next(),调用也无效,因为响应流已经结束。更常见的是把回调按业务阶段拆分:校验、取数、响应。数组不是唯一写法,也可以连续写多个参数,如app.get('/p', a, b, c)。凡是出现在路由路径后的函数型参数都会并入处理器链,顺序由书写位置决定。
一个三段式用户查询场景
假设有GET /users/:id,需要先检查id是否由数字组成,然后从内存数组取用户,最后输出JSON。若id非法,期望直接返回400,不再继续查询和响应。于是定义checkId、fetchUser、sendUser三个函数,并组合成app.get('/users/:id', checkId, fetchUser, sendUser)。checkId中解析req.params.id,发现不是正整数就res.status(400).json({error:'bad id'})并return,不调用next;合法则调用next()。
fetchUser接受next,从users数组找到匹配项。若用户不存在,则res.status(404).end()并return,避免落入sendUser;存在则把user对象挂到req.user上并next()。最后sendUser直接res.json(req.user)。这样每个阶段只做一件事,逻辑清晰。关键是每个函数都要在决定继续时显式next(),否则链条会中断。通过这种拆分,能精准控制何时终止响应,也方便单独测试某个阶段。
最容易失效的边界条件
第一,回调函数内部发生异步操作后调用next(),但外层同步代码又发响应,容易造成响应重复。例如fs.readFile后回调里res.send,但同时同步路径也可能执行res.send。此时先发的那次设置响应头,第二次会抛Cannot set headers after they are sent。因此异步操作中只要还没到最终响应,就不应提前调用res。
第二,若调用next()但后面没有更多回调,Express会继续查找后续匹配的路由;如果没有则落入默认404处理器。而调用next('route')会跳过当前路由剩余的所有回调,直接去匹配下一条路由。这常用于在某条件下换用另一个处理函数。判断何时使用普通next还是next('route'),取决于你是想继续本路由链,还是想放弃本路由。
容易答错的地方
- 只有数组能挂多个回调?
- 实际上独立参数也可,如app.get('/', a,b)同样构成链。数组只是另一种写法,可和独立参数混用。执行顺序仍按出现位置从左到右,不区分数组内外。
- 调用next()后还能安全res.send
- 先调next()再发响应是常见的重复发送错误。next()只是移交控制权,不阻止当前函数后续代码继续执行,当前函数仍可能执行res.send()。正确方式是在next()后加return,或使响应函数只出现在链尾。
面试官还会怎么问?
next('route')和普通next在这条链末尾有什么区别?
普通next()若已到链尾,控制权会去匹配下一个路由;next('route')则是立即跳过当前路由的所有剩余回调,直接匹配下一个路由,即使当前链还有回调也不再执行。
如果第一个回调里故意不调用next也不发响应,会发生什么?
请求会一直挂起,直到客户端超时断开或服务器端配置的超时触发。Express无法自动知道该跳到下一个,这通常属于编码错误,调试时需要检查是否所有路径都调用了next。
多个app.get('/x')和单次挂多个处理函数有何区别?
每次app.get都是独立注册路由。请求匹配时按定义顺序依次检查不同路由,只有当前路由链全部结束(发了响应/next到末尾)才可能到下一路由。单次挂多个处理函数属于同一路由内部,没有外层路由切换语义。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。