路由匹配过程

要点

  • 路由匹配的核心输入是请求方法 + 路径,输出是路由表中对应的处理函数链
  • 匹配优先级遵循静态路由 > 参数路由 > 通配符的顺序
  • 参数路由中的 :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 会依次匹配到:

  1. /api/* 的 logger 中间件
  2. /api/* 的 auth 中间件
  3. /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 路由器有多个实现——RegExpRouterTrieRouterSmartRouter 等。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 到底装了什么。