路由匹配过程
要点
- 路由匹配的核心输入是请求方法 + 路径,输出是路由表中对应的处理函数链
- 匹配优先级遵循静态路由 > 参数路由 > 通配符的顺序
- 参数路由中的
:id等占位符会在匹配时提取实际值,注入到 context - 一个请求可以匹配多个中间件,Hono 按注册顺序组成链式处理
- 没有命中任何路由时,流程走向
notFound兜底 - Hono 的路由器底层对路由做了分组 + 正则编译,匹配性能接近 O(1)
- 路由注册顺序会影响参数提取和通配符范围,顺序不对会踩坑
1. 匹配的基本流程
当一个请求到达 Hono,框架需要回答一个问题:「这个请求该交给谁处理?」
回答这个问题的过程就是路由匹配。输入是两样东西——请求方法(GET / POST / PUT ...)和请求路径(/users/42),输出是路由表里对应的处理函数(可能是一个,也可能是一串)。
伪代码层面大致是这样的:
// 简化后的匹配流程
function dispatch(method: string, path: string) {
// 1. 按 method 找到对应的方法路由表
const matchers = router[method]
// 2. 在方法路由表中查找 path 的匹配项
const handlers = matchers.match(path)
// 3. 没找到 → 走 notFound
if (!handlers) {
return notFound()
}
// 4. 找到了 → 按顺序执行处理函数链
return compose(handlers)
}Hono 内部把路由表按 HTTP 方法分开存储。GET 的路由和 POST 的路由各自独立,匹配时先按方法筛一层,再在子集里做路径匹配。这一步直接砍掉了大部分无关路由。
2. 匹配优先级:静态 > 参数 > 通配符
Hono 的路由有三种形态:
// 静态路由 — 路径完全固定
app.get('/users/list', handler)
// 参数路由 — 路径中有动态段
app.get('/users/:id', handler)
// 通配符路由 — 匹配某个前缀下的所有路径
app.get('/api/*', handler)当一个请求同时可能被多条路由匹配时,Hono 按以下优先级选择:
静态路由优先级最高。 路径完全一致的路由永远优先于带参数的路由。
app.get('/users/me', (c) => c.text('当前用户'))
app.get('/users/:id', (c) => c.text(`用户 ${c.req.param('id')}`))
// GET /users/me → 命中第一条,返回「当前用户」
// GET /users/42 → 命中第二条,返回「用户 42」如果你把这两条的顺序反过来写,结果不会变。静态路由的优先级是注册顺序无关的,这是 Hono 路由器内部保证的行为。
参数路由优先于通配符。 这一点在路径有重叠时很关键:
app.get('/api/*', (c) => c.text('api 通配'))
app.get('/api/health', (c) => c.text('健康检查'))
// GET /api/health → 命中静态路由,返回「健康检查」
// GET /api/users → 命中通配符,返回「api 通配」3. 参数提取::id 如何变成可用值
参数路由的核心价值在于从 URL 中提取动态数据。/users/:id 里的 :id 是一个占位符,匹配时 Hono 会把 URL 中对应位置的实际值抓出来。
app.get('/users/:id/posts/:postId', (c) => {
const id = c.req.param('id') // '42'
const postId = c.req.param('postId') // '7'
return c.json({ id, postId })
})
// GET /users/42/posts/7
// → { id: '42', postId: '7' }参数提取发生在匹配阶段。路由器在确认路径匹配的同时,把各段的值按占位符名称存入一个 map,然后注入到 context 对象。路由处理函数通过 c.req.param('name') 读取。
提取出来的值始终是字符串。如果你需要数字类型,得自己做转换:
const id = Number(c.req.param('id'))这个限制来自 URL 本身的特性——路径里的每一段都是字符串,框架没有理由替你假设类型。
4. 多中间件匹配
一个请求不一定只命中一个处理函数。中间件本质上也是路由,只不过它的路径通常是前缀匹配。
// 所有 /api 开头的请求都会经过这两个中间件
app.use('/api/*', logger)
app.use('/api/*', auth)
// 然后才到达具体路由
app.get('/api/users', listUsers)
app.get('/api/users/:id', getUser)当请求 GET /api/users 进来时,Hono 会依次匹配到:
/api/*的 logger 中间件/api/*的 auth 中间件/api/users的 listUsers 处理函数
这三个函数组成一个链,按注册顺序依次执行。每个中间件内部调用 await next() 把控制权交给下一个,执行完再回来做收尾工作。这就是 Hono 的洋葱模型,会在后面的中间件专题里详细讲。
关键是:匹配阶段就已经确定了这条链的完整内容。不是执行完 logger 再临时去查下一个是谁,而是一开始就全部找好了。
5. 匹配失败:走向 notFound
如果请求的路径在路由表里找不到任何匹配,Hono 会走 notFound 逻辑。
app.notFound((c) => {
return c.json({ error: 'Not Found' }, 404)
})如果你没有显式注册 notFound 处理函数,Hono 会返回一个默认的 404 响应。
这里有一个容易忽略的细节:方法不匹配也会走 notFound,而不是 405。也就是说,你注册了 app.post('/users', handler),但发了一个 GET /users 请求,Hono 会告诉你 404 而不是 405。这是框架当前的行为,如果你需要严格区分 404 和 405,需要自己在中间件里处理。
6. 性能考量:为什么 Hono 路由匹配快
Hono 的路由匹配性能在同类框架中属于第一梯队。原因在于它的路由器实现做了两件事。
第一,按方法分表。 匹配时先用 HTTP 方法过滤,减少搜索范围。
第二,路由分组 + 正则编译。 Hono 不是对每条路由逐一做字符串比对。它在注册阶段就把同类型的路由归入分组,编译成高效的匹配结构。对于参数路由和通配符路由,Hono 会把路径模式转成正则表达式,在匹配时一次正则测试就能判断是否命中。
// 注册阶段(简化示意)
// '/users/:id' → 编译为 /^\/users\/([^/]+)$/
// '/api/*' → 编译为 /^\/api\/(.*)$/这意味着匹配过程的时间复杂度不随路由数量线性增长。即使你注册了 500 条路由,单次匹配仍然只需要几次正则测试。
Hono 路由器有多个实现——RegExpRouter、TrieRouter、SmartRouter 等。SmartRouter 会根据路由注册情况自动选择最优的路由器。默认配置下你不需要关心这些细节,框架已经帮你做了选择。
7. 常见匹配陷阱
路径冲突
两条路由如果模式相同但参数名不同,后注册的会覆盖前面的:
app.get('/users/:id', handlerA)
app.get('/users/:userId', handlerB)
// handlerA 永远不会被调用这类问题在多人协作的项目里偶尔出现。路由命名要统一,同一个路径模式只用一套参数名。
通配符范围过大
通配符路由会「吃掉」它范围内的所有请求。如果你在文件开头就注册了一个宽泛的通配符,后面注册的精确路由可能被跳过:
// 这样写会出问题
app.get('/api/*', catchAll)
app.get('/api/users', listUsers) // 永远不会被调用但实际上 Hono 的静态路由优先级高于通配符,所以上面的例子中 /api/users 仍然会被正确匹配到 listUsers。真正会出问题的是两个通配符之间的冲突:
app.get('/api/*', broadMatch)
app.get('/api/v2/*', v2Match)
// GET /api/v2/users → 匹配 /api/*(先注册,先匹配)同优先级的路由按注册顺序匹配,先注册的先命中。如果你需要 /api/v2/* 优先处理,必须把它注册在 /api/* 前面。
尾部斜杠
/users 和 /users/ 在 Hono 里是两条不同的路由。如果你的前端请求带了尾部斜杠,但路由注册时没带,就会 404。
这个问题没有银弹,建议在项目初期就约定好是否统一使用尾部斜杠,然后保持一致。
延伸阅读
总结
路由匹配的过程可以压缩成一句话:按方法分表,按优先级匹配,静态优先于参数优先于通配符,匹配结果组成处理链交给执行引擎。
理解这个过程之后,很多实际开发中的疑惑就能自己回答了——为什么这条路由没命中、为什么参数取不到值、为什么中间件没执行。答案通常都在匹配规则和注册顺序里。
下一篇讲 Context 上下文对象,也就是匹配成功后,处理函数拿到的那个 c 到底装了什么。