简介 当你的 API 端点数量超过寥寥几个时,把所有东西都塞进单个文件很快就会变得难以管理。Express 提供了一个内置工具 Router,让你按资源将路由拆分到不同的模块中。配合控制器层,你就拥有了一个能够干净扩展的结构——新成员可以快速找到需要的东西,新增资源也不会触动已有代码。
本课将带你走过一个具体的项目布局:路由负责定义 URL 模式和 HTTP 方法,控制器承载实际的处理逻辑,app.js 负责将它们串联起来。这种分层习惯是越早建立越重要的一项基本功。
项目结构 下面是我们将要搭建的目录布局:
text 复制
project/
├── app.js
├── routes/
│ ├── users.js
│ └── products.js
└── controllers/
├── usersController.js
└── productsController.js
模式很简单:每个资源对应一个路由文件,每个资源对应一个控制器文件。路由文件保持精简——只声明每个端点由哪个控制器函数处理;控制器保持专注——只包含针对该资源的请求/响应逻辑。
Express Router Express Router 会创建一个迷你应用来独立处理路由,然后你在 app.js 中以某个路径将其挂载。
首先编写 users 路由文件:
javascript 复制
const express = require ( 'express' ) ;
const router = express . Router ( ) ;
const usersController = require ( '../controllers/usersController' ) ;
router . get ( '/' , usersController . getAllUsers ) ;
router . get ( '/:id' , usersController . getUserById ) ;
router . post ( '/' , usersController . createUser ) ;
router . put ( '/:id' , usersController . updateUser ) ;
router . delete ( '/:id' , usersController . deleteUser ) ;
module . exports = router ;
注意路由中使用的是相对路径,例如 / 和 /:id,而不是 /users 或 /users/:id。前缀会在 app.js 中挂载 router 时统一加上,这样你就无需重复书写。
在 app.js 中挂载该 router:
javascript 复制
const express = require ( 'express' ) ;
const app = express ( ) ;
app . use ( express . json ( ) ) ;
const usersRouter = require ( './routes/users' ) ;
const productsRouter = require ( './routes/products' ) ;
app . use ( '/users' , usersRouter ) ;
app . use ( '/products' , productsRouter ) ;
app . listen ( 3000 , ( ) => console . log ( 'Server running on port 3000' ) ) ;
现在,对 /users/42 发出的 GET 请求会匹配到 router.get('/:id', ...),且 req.params.id 等于 '42'。清晰且可预测。
控制器 控制器就是普通的模块,以具名函数的形式导出。每个函数都是一个标准的 Express 请求处理函数——接收 req 和 res(以及可选的 next),并负责发送响应。
javascript 复制
const users = [
{ id : 1 , name : 'Alice' , email : 'alice@example.com' } ,
{ id : 2 , name : 'Bob' , email : 'bob@example.com' } ,
] ;
exports . getAllUsers = ( req , res ) => {
res . json ( users ) ;
} ;
exports . getUserById = ( req , res ) => {
const user = users . find ( u => u . id === parseInt ( req . params . id ) ) ;
if ( ! user ) {
return res . status ( 404 ) . json ( { message : 'User not found' } ) ;
}
res . json ( user ) ;
} ;
exports . createUser = ( req , res ) => {
const { name , email } = req . body ;
if ( ! name || ! email ) {
return res . status ( 400 ) . json ( { message : 'name and email are required' } ) ;
}
const newUser = { id : users . length + 1 , name , email } ;
users . push ( newUser ) ;
res . status ( 201 ) . json ( newUser ) ;
} ;
exports . updateUser = ( req , res ) => {
const user = users . find ( u => u . id === parseInt ( req . params . id ) ) ;
if ( ! user ) {
return res . status ( 404 ) . json ( { message : 'User not found' } ) ;
}
const { name , email } = req . body ;
if ( name ) user . name = name ;
if ( email ) user . email = email ;
res . json ( user ) ;
} ;
exports . deleteUser = ( req , res ) => {
const index = users . findIndex ( u => u . id === parseInt ( req . params . id ) ) ;
if ( index === - 1 ) {
return res . status ( 404 ) . json ( { message : 'User not found' } ) ;
}
users . splice ( index , 1 ) ;
res . status ( 204 ) . send ( ) ;
} ;
这里有几点值得注意。控制器会校验输入,发现问题时立即以合适的状态码提前返回——这让正常路径的代码更易阅读。404 检查是显式进行的:不要假设记录一定存在。而成功的删除操作,其约定俗成的响应是 204 No Content 且不返回响应体。
常见错误 忘记解析 JSON 请求体是初学者最常遇到的问题。如果 app.js 中没有 app.use(express.json()),req.body 将是 undefined,createUser 或 updateUser 会在悄无声息中收到空数据。
另一个常见坑是把 req.params.id(字符串类型)直接用严格相等与数字类型的 id 比较。users.find(u => u.id === req.params.id) 这样的检查永远会失败,因为 '42' !== 42。请先用 parseInt 或 Number() 进行转换。
最后,发送响应后没有 return 会导致 "Cannot set headers after they are sent"(无法在已发送响应头之后再次设置)错误。务必使用 return res.status(404).json(...)——return 能阻止 Express 继续执行后续代码、避免重复发送响应。
小结 Express Router 让你将相关的路由归入各自独立的模块,并在 app.js 中以某个前缀挂载它们。控制器把处理逻辑从路由文件中剥离出来,使每一层都更易阅读、测试和扩展。整体模式是:路由文件声明 URL 与方法,控制器函数处理逻辑并发送响应。坚持每个资源对应一个文件,随着项目增长,这样的结构依然稳固可靠。