路由分组

要点

  • app.route() 是 Hono 路由分组的核心机制,将子 Hono 实例挂载到父 app 的指定路径前缀下
  • app.route('/api', subApp)app.basePath('/api') 都能加前缀,但内部机制不同
  • 多层 app.route() 可以嵌套,路径前缀逐层叠加
  • 路由分组的注册顺序决定匹配优先级,先注册的挂载点先被检查
  • 父 app 的中间件向下穿透到所有子 app,Context 在整个挂载链中共享
  • 按功能模块分组比按技术层分组更适合大多数项目的演进节奏
  • 同一挂载路径可以注册多个不同 HTTP 方法的子 app,互不冲突

1. 什么是路由分组

路由超过十来个之后,全部写在同一个文件里会变得难以维护。更实际的问题在于:相关的路由之间通常共享业务逻辑,但和其他路由没有关系。把用户管理和文章管理的路由混在一起,既不方便查找,也不方便独立测试。

路由分组要解决的问题:把逻辑上相关的路由归到一组,给它们统一的路径前缀,同时让每个分组可以独立管理自己的路由和中间件。

Hono 提供两种分组手段:

  1. app.route(path, subApp) — 将子 Hono 实例挂载到指定路径
  2. app.basePath(path) — 返回带统一前缀的新 Hono 实例

2. app.route() 的工作原理

app.route() 接收挂载路径和一个 Hono 子实例:

// users.ts
import { Hono } from 'hono'
 
const users = new Hono()
users.get('/', (c) => c.json({ users: [] }))
users.get('/:id', (c) => c.json({ id: c.req.param('id') }))
users.post('/', (c) => c.json({ message: 'created' }, 201))
 
export default users
// index.ts
import { Hono } from 'hono'
import users from './users'
 
const app = new Hono()
app.route('/users', users)
 
export default app

挂载之后,users 子 app 中的 GET / 变成了 GET /users/GET /:id 变成了 GET /users/:id。子 app 不需要知道自己被挂在哪个前缀下。

内部过程是:app.route('/users', users) 执行时,Hono 遍历 users 上已注册的所有路由,将每条路由路径拼接上挂载前缀 /users,注册到父 app 的路由表。拼接规则是简单的字符串连接。

子 app 的路由必须在 app.route() 调用之前全部注册完毕。 app.route() 在调用时快照子 app 当前的路由表,之后再追加的路由不会同步到父 app。

// ❌ 错误顺序 — users.get() 在 app.route() 之后,路由不会生效
app.route('/users', users)
users.get('/', (c) => c.json({ users: [] }))
 
// ✅ 正确顺序 — 先定义路由,再挂载
users.get('/', (c) => c.json({ users: [] }))
app.route('/users', users)

3. app.route() 与 basePath() 的区别

app.basePath('/api') 返回一个新的 Hono 实例,该实例上注册的所有路由自动加 /api 前缀:

// index.ts
const app = new Hono().basePath('/api')
app.get('/users', (c) => c.json({ users: [] }))
// 实际注册:GET /api/users

app.route('/api', subApp) 则是把已有子 app 挂到父 app 的 /api 路径下:

// index.ts
const subApp = new Hono()
subApp.get('/users', (c) => c.json({ users: [] }))
 
const app = new Hono()
app.route('/api', subApp)
// subApp 的 GET /users → GET /api/users

两者最终路由效果相同,区别在组织方式:

维度basePath()app.route()
返回值新 Hono 实例无返回值(挂载动作)
子 app 独立性无独立子 app子 app 可独立定义、测试
多层嵌套不支持支持
适用场景单一前缀,扁平路由模块化拆分,按业务域分组

两者可以组合使用:

// index.ts
const app = new Hono().basePath('/api/v1')
app.route('/users', users) // GET /api/v1/users/
app.route('/posts', posts) // GET /api/v1/posts/

主 app 用 basePath() 统一处理版本前缀,业务模块通过 app.route() 挂载。

4. 多层分组与路径叠加

app.route() 支持多层嵌套,路径前缀逐层叠加:

// routes/admin/orders.ts
import { Hono } from 'hono'
 
const orders = new Hono()
orders.get('/', (c) => c.json({ orders: [] }))
orders.get('/:id', (c) => c.json({ orderId: c.req.param('id') }))
export default orders
// routes/admin/index.ts
import { Hono } from 'hono'
import orders from './orders'
 
const admin = new Hono()
admin.route('/orders', orders)
export default admin
// index.ts
import { Hono } from 'hono'
import admin from './routes/admin'
 
const app = new Hono()
app.route('/api/v1', admin)
// admin 的 GET /orders/ → GET /api/v1/orders/
// admin 的 GET /orders/:id → GET /api/v1/orders/:id
export default app

三层嵌套的路径叠加过程:orders 子 app: GET /admin 加前缀 /orders: GET /orders/app 加前缀 /api/v1: GET /api/v1/orders/。规则是字符串拼接,任意层数下一致。

注意避免路径重复:

// ❌ 子 app 路径已包含前缀,会导致 GET /users/users
const users = new Hono()
users.get('/users', (c) => c.json({ users: [] }))
app.route('/users', users)
 
// ✅ 子 app 内使用相对路径
const users = new Hono()
users.get('/', (c) => c.json({ users: [] }))
app.route('/users', users) // GET /users/

子 app 内部的路径应该相对于自身的挂载点来写。

5. 挂载顺序与匹配优先级

Hono 路由匹配遵循「先注册先匹配」原则。多个 app.route() 挂载到重叠前缀时,注册顺序直接影响匹配行为。

// index.ts
const api = new Hono()
api.get('/users', (c) => c.json({ users: [] }))
 
const admin = new Hono()
admin.get('/dashboard', (c) => c.json({ stats: {} }))
 
const app = new Hono()
app.route('/api', api)         // 先注册
app.route('/api/admin', admin) // 后注册

请求 GET /api/admin/dashboard 到达时,路由器先检查 /api,前缀匹配成功,进入 api 子 app。在 api 内部剩余路径是 /admin/dashboard,没有匹配路由,最终返回 404。

挂载路径有包含关系时,更具体的前缀应该先注册。

// ✅ 更具体的前缀先注册
app.route('/api/admin', admin) // 先注册
app.route('/api', api)          // 后注册

调整后 GET /api/admin/dashboard 先匹配 /api/admin,正确进入 admin 子 app。GET /api/users 不匹配 /api/admin,继续检查到 /api,正常处理。

挂载路径完全不同或无包含关系时,注册顺序不影响匹配结果。

6. 按功能分组 vs 按技术层分组

两种分组思路:

按功能模块分组 — 每个业务域一个子 app:

routes/
├── users.ts      # 用户相关
├── posts.ts      # 文章相关
├── orders.ts     # 订单相关
└── comments.ts   # 评论相关

按技术层分组 — 每类技术职责一个子 app:

routes/
├── api.ts        # 对外 API
├── admin.ts      # 管理后台
├── webhooks.ts   # Webhook 接收
└── internal.ts   # 内部调用

按功能模块分组在多数情况下更容易维护——某个业务域需要改动时,相关代码在同一个文件里,不需要跨多个技术层查找。

两种思路可以组合:

// routes/admin/index.ts
import { Hono } from 'hono'
import adminUsers from './users'
import adminOrders from './orders'
import adminStats from './stats'
 
const admin = new Hono()
admin.use('*', adminAuthMiddleware) // 管理后台统一鉴权
admin.route('/users', adminUsers)
admin.route('/orders', adminOrders)
admin.route('/stats', adminStats)
export default admin

注意一个边界:按技术层分组时,中间件作用域容易超出预期。把认证中间件挂在混合了多个业务域的 api 子 app 上,一旦某个域需要不同的认证策略,就得在中间件里加条件分支,增加复杂度。

7. Context 共享与中间件穿透

app.route() 挂载子 app 时,Context 在整个请求链路中共享。父 app 中间件通过 c.set() 设置的值,子 app 路由可以通过 c.get() 读到。

// middleware/auth.ts
import { createMiddleware } from 'hono/factory'
 
export const authMiddleware = createMiddleware<{
  Variables: { userId: string }
}>(async (c, next) => {
  const token = c.req.header('Authorization')
  // 验证 token ...
  c.set('userId', 'user-42')
  await next()
})
// routes/users.ts
import { Hono } from 'hono'
 
const users = new Hono()
users.get('/me', (c) => {
  const userId = c.get('userId') // 直接读取父 app 中间件设置的值
  return c.json({ userId })
})
export default users
// index.ts
import { Hono } from 'hono'
import { authMiddleware } from './middleware/auth'
import users from './routes/users'
 
const app = new Hono()
app.use('/api/*', authMiddleware)
app.route('/api/users', users)
export default app

请求 GET /api/users/me 的执行顺序:authMiddleware 执行并 c.set('userId', 'user-42') → 进入 users 子 app 的路由 → c.get('userId') 读到 'user-42'。不需要额外的参数传递。

完整的中间件穿透流程:

// index.ts
const app = new Hono()
 
app.use('*', logger)              // 全局中间件
app.route('/api', apiApp)         // api 内部有 auth 中间件
app.route('/public', publicApp)   // 只有全局 logger
app.route('/admin', adminApp)     // admin 内部有 auth + adminOnly

请求 GET /admin/users 的中间件链:

请求 → logger(全局)→ auth(admin 子 app)→ adminOnly(admin 子 app)→ 路由处理函数
响应 ← logger ← auth ← adminOnly ← 处理函数

中间件穿透规则:

  1. 父 app 的 app.use() 中间件对所有子 app 生效
  2. 子 app 内部中间件只对该子 app 的路由生效
  3. 执行顺序从外到内:父 app 中间件先执行,子 app 后执行
  4. 响应阶段反向

同一个中间件既挂父 app 又挂子 app 会执行两次,通常不是期望的行为。

8. 不同规模项目的分组模式

小型项目(5-10 个路由) — 不需要拆分文件,basePath() 加扁平路由即可:

// index.ts
const app = new Hono().basePath('/api/v1')
app.get('/users', (c) => c.json({ users: [] }))
app.get('/users/:id', (c) => c.json({ id: c.req.param('id') }))
app.get('/posts', (c) => c.json({ posts: [] }))
export default app

中型项目(10-50 个路由) — 按业务域拆分文件,每个文件导出独立子 app:

// index.ts
const app = new Hono().basePath('/api/v1')
app.use('*', cors())
app.route('/users', users)
app.route('/posts', posts)
app.route('/auth', auth)
export default app

大型项目(50+ 个路由) — 引入多层分组,按业务域 → 子模块两级拆分:

routes/
├── users/
│   ├── index.ts      # 模块入口,注册中间件
│   ├── profile.ts    # 个人资料子路由
│   └── settings.ts   # 设置子路由
├── posts/
│   ├── index.ts
│   └── comments.ts
└── admin/
    ├── index.ts
    └── orders.ts
// routes/users/index.ts
import { Hono } from 'hono'
import profile from './profile'
import settings from './settings'
 
const users = new Hono()
users.route('/profile', profile)
users.route('/settings', settings)
users.get('/', (c) => c.json({ users: [] }))
export default users

项目规模继续增大时,可以考虑按需加载路由模块——用动态 import 在请求到达时再加载对应路由文件,减少冷启动时间,但会增加首次请求延迟。

9. 同一挂载路径的多方法分组

Hono 路由匹配同时考虑路径和 HTTP 方法,同一挂载路径可以注册多个不同方法的子 app:

// routes/users-read.ts
import { Hono } from 'hono'
 
const read = new Hono()
read.get('/', (c) => c.json({ users: [] }))
read.get('/:id', (c) => c.json({ id: c.req.param('id') }))
export default read
// routes/users-write.ts
import { Hono } from 'hono'
 
const write = new Hono()
write.post('/', (c) => c.json({ message: 'created' }, 201))
write.put('/:id', (c) => c.json({ message: 'updated' }))
write.delete('/:id', (c) => c.json({ message: 'deleted' }))
export default write
// index.ts
import { Hono } from 'hono'
import { authMiddleware } from './middleware/auth'
import readRoutes from './routes/users-read'
import writeRoutes from './routes/users-write'
 
const app = new Hono()
 
// 读操作不需要认证
app.route('/users', readRoutes)
 
// 写操作需要认证
app.use('/users/*', authMiddleware)
app.route('/users', writeRoutes)
 
export default app

读操作和写操作拆到不同子 app,挂载到同一个路径 /usersapp.use('/users/*', authMiddleware) 写在读操作路由之后、写操作路由之前,所以只对写操作生效。

这个模式在需要为不同操作设置不同中间件策略时有用。如果不需要区分中间件,直接在同一个子 app 里注册所有方法即可。

延伸阅读

总结

路由分组是 Hono 组织大型应用的基础手段。app.route() 支持独立子 app 和多层嵌套,basePath() 更适合扁平的统一前缀场景。多层嵌套时路径前缀逐层叠加,子 app 内部路径应相对于挂载点来写。注册顺序影响匹配优先级——挂载路径有包含关系时,更具体的前缀先注册。中间件从父 app 向下穿透,Context 在整个链路中共享。不同规模项目适合不同的分组模式,从小型的扁平路由到大型的多层分组。

下一篇(06-模块化路由拆分)继续讨论如何把路由模块进一步拆到独立的目录结构中,以及如何处理模块间的中间件共享和类型传递。