Express 路由
本教程共 76 篇 · 第 39 篇 · 更新于 2026-07-25 · 约 5 分钟阅读
39. Express 路由
本节目标:路由方法、参数、查询字符串和 Router 拆分。
路由是 Web 框架的脊梁。没有它,所有请求都砸向同一个回调,代码很快变成一碗面条。Express 的路由系统既简单直白,又足够灵活——小到一行回调,大到按业务模块拆分,都能应付。
路由基础
路由的本质是「匹配路径 + HTTP 方法 → 执行处理函数」。Express 为每种 HTTP 方法都提供了对应的方法:
import express from 'express'
const app = express()
app.get('/', (req, res) => res.send('GET 首页'))
app.post('/users', (req, res) => res.send('创建用户'))
app.put('/users/:id', (req, res) => res.send('更新用户'))
app.delete('/users/:id', (req, res) => res.send('删除用户'))
app.patch('/users/:id', (req, res) => res.send('部分更新'))
如果你希望同一条路径响应所有方法,用 app.all()。注意 Express 5 的写法:
// 对 /health 路径响应任何 HTTP 方法
app.all('/health', (req, res) => {
res.json({ status: 'ok', method: req.method })
})
Tip
app.all('/*splat')可以放在所有路由最后面当「兜底」404,捕获前面没匹配到的所有请求。顺序很重要——Express 按代码出现的先后顺序匹配,谁在前谁先赢。
路由参数
URL 里经常带有动态 ID,比如 /users/42、/posts/2024/summary。Express 用冒号 : 标记参数段:
app.get('/users/:userId/books/:bookId', (req, res) => {
res.json({
userId: req.params.userId,
bookId: req.params.bookId
})
})
访问 /users/42/books/7 会得到 {"userId":"42","bookId":"7"}。参数值永远是字符串,如果需要数字自己转换:
const id = Number(req.params.userId)
if (Number.isNaN(id)) {
return res.status(400).json({ error: 'Invalid user id' })
}
参数名里不能用连字符 -,因为会被当成减号解析。如果 URL 里真有带横线的标识,改用下划线或者放在查询字符串里。
查询字符串
路由参数用来定位资源,查询字符串(query string)用来过滤、排序、分页。Express 自动帮你解析 ? 后面的内容:
app.get('/products', (req, res) => {
const { category, page = '1', limit = '10' } = req.query
res.json({
category,
page: Number(page),
limit: Number(limit)
})
})
访问 /products?category=phone&page=2&limit=20,req.query 就是一个普通对象:
{ "category": "phone", "page": "2", "limit": "20" }
Warning
req.query的值全是字符串。如果你拿它做数学运算或数据库查询,记得先转类型,不然page + 1会变成"21"而不是3。
路由拆分:Router
接口一多,app.js 就会膨胀。Express 提供了 express.Router(),让你按业务模块拆路由文件。
假设项目结构如下:
routes/
├── users.js
└── posts.js
app.js
routes/users.js:
import express from 'express'
const router = express.Router()
router.get('/', (req, res) => {
res.json([{ id: 1, name: 'tom' }])
})
router.get('/:id', (req, res) => {
res.json({ id: req.params.id, name: 'tom' })
})
router.post('/', (req, res) => {
res.status(201).json({ message: 'user created' })
})
export default router
app.js 里挂载:
import express from 'express'
import userRoutes from './routes/users.js'
const app = express()
app.use('/api/users', userRoutes)
这样 /api/users 指向用户列表,/api/users/42 指向单个用户。Router 本身也是一个中间件,你可以给它加专属的前置逻辑:
router.use((req, res, next) => {
console.log('Users router hit:', req.url)
next()
})
这段日志只会在访问 /api/users/* 时触发,不会影响别的路由。
路由匹配规则
Express 的路由匹配不只是「字符串相等」,它支持一些模式语法:
// ? 表示前面的字符可选
app.get('/ab?cd', handler) // 匹配 /abcd 和 /acd
// + 表示前面的字符重复一次或多次
app.get('/ab+cd', handler) // 匹配 /abcd, /abbcd, /abbbcd ...
// () 分组
app.get('/a(bc)?d', handler) // 匹配 /ad 和 /abcd
正则也能直接用:
// 匹配包含 "fly" 的路径
app.get(/.*fly$/, (req, res) => {
res.send(req.path)
})
不过实际项目中,99% 的场景用普通字符串和 : 参数就够了。复杂的正则路由虽然强大,但可读性差,维护时容易踩坑。
多个处理函数与 next()
一条路由可以挂多个处理函数,它们按顺序执行。关键在于第三个参数 next():
app.get('/demo',
(req, res, next) => {
console.log('第一步')
req.timestamp = Date.now()
next() // 把控制权交给下一个函数
},
(req, res) => {
console.log('第二步,timestamp:', req.timestamp)
res.send('Done')
}
)
如果你忘了调用 next(),请求就会挂在那里直到超时。反之,如果你已经调用了 res.send() 又跑去调用 next(),Express 会抛「Cannot set headers after they are sent」错误。这个坑我踩过不止一次。
路由顺序的陷阱
Express 的路由匹配是「先匹配先返回」,所以宽泛的路径要放在后面。
// 错误示范
app.get('/:id', (req, res) => res.send('任意ID'))
app.get('/users', (req, res) => res.send('用户列表'))
这段代码里,访问 /users 永远不会走到第二条路由,因为 /users 已经被 /:id 截获了。正确顺序是:
app.get('/users', handler)
app.get('/:id', handler)
Tip把精确路由放前面,参数路由放中间,兜底路由放最后。这既是 Express 的要求,也是所有路由框架的通用原则。