9.1 Router 层是什么
Router 层负责把 URL 和 HTTP 方法映射到对应的 Controller 方法。
Express 的Router可以把路由分组管理,而不是把所有路由都堆在app.ts里。
9.2 基本用法
最简单的写法
import{Router}from'express';import{ProjectController}from'@/controllers/project/project-controller.js';exportfunctioncreateProjectRouter(controller:ProjectController){constrouter=Router();router.get('/projects',controller.list);router.get('/projects/:id',controller.get);router.post('/projects',controller.create);router.put('/projects/:id',controller.update);router.delete('/projects/:id',controller.remove);returnrouter;}用前缀分组
同一个模块的路由共享前缀:
exportfunctioncreateProjectRouter(controller:ProjectController){constrouter=Router();// 所有路由自动带 /projects 前缀router.get('/',controller.list);// GET /projectsrouter.get('/:id',controller.get);// GET /projects/:idrouter.put('/',controller.create);// PUT /projectsrouter.put('/:id',controller.update);// PUT /projects/:idrouter.delete('/:id',controller.remove);// DELETE /projects/:idreturnrouter;}然后在父路由里挂上前缀:
router.use('/projects',createProjectRouter(projectController));9.3 路由的层级组织
按版本 + 按模块
大型项目推荐这样组织:
src/routers/ ├── index.ts# 根路由,挂载所有子路由├── api-router.ts# /api 总路由├── v1/ │ └── index.ts# /api/v1 版本路由├── v2/ │ └── index.ts# /api/v2 版本路由├── project/ │ └── project-router.ts# 项目管理模块路由└── user/ └── user-router.ts# 用户管理模块路由根路由:src/routers/index.ts
import{Router}from'express';import{createApiRouter}from'./api-router.js';exportfunctioncreateRouter(){constrouter=Router();// API 路由router.use('/api',createApiRouter());// 健康检查(不放在 /api 下也行)router.get('/health',(_req,res)=>{res.json({status:'ok'});});returnrouter;}API 总路由:src/routers/api-router.ts
import{Router}from'express';import{createV1Router}from'./v1/index.js';exportfunctioncreateApiRouter(){constrouter=Router();router.use('/v1',createV1Router());// router.use('/v2', createV2Router()); // 以后加 v2 版本returnrouter;}版本路由:src/routers/v1/index.ts
import{Router}from'express';importtype{AppModels}from'@/bootstrap.js';import{ProjectController}from'@/controllers/project/project-controller.js';import{UserController}from'@/controllers/user/user-controller.js';import{ProjectService}from'@/services/project/project-service.js';import{UserService}from'@/services/user/user-service.js';import{createProjectRouter}from'../project/project-router.js';import{createUserRouter}from'../user/user-router.js';exportfunctioncreateV1Router(models:AppModels){constrouter=Router();// 组装各模块的 Controller 和 ServiceconstprojectController=newProjectController(newProjectService(models.projects),);constuserController=newUserController(newUserService(models.users),);// 挂载各模块路由router.use('/projects',createProjectRouter(projectController));router.use('/users',createUserRouter(userController));returnrouter;}模块路由:src/routers/project/project-router.ts
import{Router}from'express';importtype{ProjectController}from'@/controllers/project/project-controller.js';exportfunctioncreateProjectRouter(controller:ProjectController){constrouter=Router();router.get('/',controller.list);router.get('/:id',controller.get);router.put('/',controller.create);router.put('/:id',controller.update);router.delete('/:id',controller.remove);returnrouter;}9.4 为什么在版本路由里组装 Controller
你可能注意到了:Controller 和 Service 的组装(new)放在了版本路由里,而不是bootstrap.ts。
两种做法都可以:
| 做法 | 优点 | 缺点 |
|---|---|---|
| bootstrap 里组装全部 | 统一管理,所有依赖一目了然 | bootstrap 文件会越来越长 |
| 各版本路由里组装 | 每个版本自己决定用哪些模块,不同版本可以有不同实现 | 依赖分散在多处 |
中小项目推荐放 bootstrap 统一管理,大项目可以按模块/按版本分散。
本教程为了简单清晰,采用集中在 bootstrap 组装的方式(见第 10 篇)。
9.5 路由级中间件
除了全局中间件,还可以给某个路由或某组路由单独加中间件。
给单个路由加
router.get('/projects',authMiddleware,controller.list);给一组路由加
constrouter=Router();// 这些不需要登录router.post('/login',authController.login);router.post('/register',authController.register);// 下面的都需要登录router.use(authMiddleware);router.get('/projects',controller.list);router.get('/projects/:id',controller.get);给子路由加
// 管理后台的路由都需要管理员权限router.use('/admin',requireAdmin,createAdminRouter(adminController));路由级中间件的执行顺序是:先执行全局的,再依次执行路由上挂载的。
9.6 路由参数的类型
req.params里的参数都是字符串,哪怕你定义的是数字 ID。
router.get('/projects/:id',(req,res)=>{console.log(typeofreq.params.id);// "string"// 用的时候要转成数字constid=Number(req.params.id);});这也是为什么我们在 Controller 里用Number(req.params.id)转一下。
9.7 常见的路由组织问题
❌ 把业务逻辑写在路由文件里
// 反面例子router.get('/projects',async(req,res)=>{// 写了一堆业务逻辑constdb=awaitgetDB();constlist=awaitdb.query('SELECT * FROM projects');res.json({status:0,data:list});});路由文件只负责 URL 映射,业务逻辑放在 Controller 和 Service。
❌ 路由层级太深
/api/v1/admin/settings/notification/email/template/list层级太多不便于理解和查找。一般控制在 3-4 层以内比较合适。
❌ 同一个接口有多种写法
// 一会用 get,一会用 post,参数位置也不统一router.get('/project/list',...);router.post('/project/add',...);router.get('/project/detail/:id',...);保持风格一致,要么都 RESTful,要么都用动词后缀。
9.8 小结
- Router 层负责 URL 到 Controller 的映射
- 按版本 + 按模块组织路由,结构清晰
- 路由文件只写映射关系,不写业务逻辑
- Controller 可以在 bootstrap 集中组装,也可以在各路由里分散组装
- 路由级中间件用于给部分接口加特定功能(如鉴权、限流)