先记住这个答案
Router 是一个完整的中间件和路由系统,可视为迷你应用。在 app.use(path, router) 中,path 成为 router 内所有路由的前缀。例如 router.get('/home') 挂载到 '/api' 后对应 '/api/home'。router 内部无需感知前缀,Express 在匹配时会自动拼接。若父路径含参数,需设置 mergeParams 才能访问父路径参数。
- Router 是可独立挂载的迷你路由系统
- 路径前缀由 app.use 的第一个参数决定
- 父路径参数需 mergeParams 才能取到
Router 如何获得前缀
express.Router() 返回一个独立的路由容器,它内部可以注册中间件和路由处理函数。当通过 app.use('/api', router) 挂载时,Express 会把所有发给 /api 的请求交给 router 处理,且路由器并不需要知道前缀。它的每个路由只定义相对路径,最终匹配由挂载点和相对路径拼接而成。
例如 router.get('/product') 挂载到 /api 后,实际匹配 GET /api/product;而 router.get('/') 则匹配 GET /api。Express 内部将挂载点与路由路径拼接成完整路径模式,并自动处理斜杠边界。在 router 内部,req.url 会被裁剪掉挂载前缀,因此 router 内的路径匹配是基于相对路径的。
这种机制让 Router 成为可复用的中间件层。你可以在一个 router 中定义多个子路径,而无须担心外部挂载的上下文,前缀完全由根应用控制,这为代码解耦提供了基石。
拆分 REST 路由的实际场景
假设一个服务有用户和订单两组资源,每组有若干接口。若全部写进 app.js 会难以维护。可以创建 userRouter.js,内部只写 router.get('/profile')、router.get('/orders') 等相对路径。然后 app.use('/users', userRouter) 和 app.use('/orders', orderRouter)。
这里要注意的是:用户相关接口挂 /users 前缀,订单相关挂 /orders。router 内部不需要重复写前缀。若某个资源需要多级路径,例如访问 GET /users/:userId/orders,可挂载 '/users',router 内定义 '/:userId/orders',此时 :userId 是 router 内部参数,可直接在 req.params 中获取;只有当父路径本身带参数(如 app.use('/users/:userId', router) 且 router 内定义 '/orders')时,默认无法在 router 内访问父路径的 :userId,需设置 { mergeParams: true } 才能取到。
实际决策:是按资源模块拆分还是按业务域拆分。每个模块挂不同前缀,接口结构清晰,也方便为特定前缀加验证中间件,如 app.use('/admin', adminAuth, adminRouter)。
前缀拼接的失效条件
当父路由本身含路径参数,如 app.use('/users/:userId', router) 时,router 内部默认无法在 req.params 中读取 :userId,除非传入 { mergeParams: true }。若不设置,router 内 req.params 仅包含自己路由定义的参数,这是 Express 的默认安全设计,以避免参数名冲突。
另外,若将同一 router 多次挂载到不同前缀,如 app.use('/a', router) 和 app.use('/b', router),router 内的路由会同时响应两个前缀。这通常不符合预期,因为模块通常针对单一前缀;若确实需要多前缀复用,应显式创建多个 Router 实例或使用工厂函数生成。
还需注意挂载顺序:app.use 按声明顺序处理,若前面有 app.use('/', someMiddleware) 可能拦截请求,但不会影响路径匹配,只影响是否执行后续挂载。
容易答错的地方
- 误认为 Router 必须是单独文件
- 有人认为必须把 router 导出成独立模块,其实 express.Router 只是对象,可以在任意模块中创建再挂载。拆分核心诉求是代码组织,路径前缀由 app.use 决定,与文件结构无关。
- 在 router 内部写完整前缀
- 有些人在 router 内部写完整路径,如 router.get('/api/user'),再挂载到 '/',这样虽然能工作,但违背模块化初衷。正确做法是 router 只写相对路径,让 app.use 统一控制前缀,便于迁移与复用。
面试官还会怎么问?
app.use('/api', router1, router2) 会怎样?
app.use 可接受多个处理函数,会依次执行。但通常每个 router 设计为处理完整请求,因此连续挂载多个 router 没有实际意义;若需先做过滤,应显式写中间件函数再调用 router。
router 内部的中间件路径如何匹配?
router.use() 未给路径时匹配所有进入该 router 的请求;若给出路径如 router.use('/admin'),则匹配挂载点 + '/admin' 起始的请求。这与路由定义保持一致。
Router 还支持哪些路径配置选项?
常见有 caseSensitive、strict 等同 app 设置。mergeParams 用于访问父路由参数。它们影响 router 内部的路径匹配行为,需按实际需求去设置。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。