路由分组
要点
app.route()是 Hono 路由分组的核心机制,将子 Hono 实例挂载到父 app 的指定路径前缀下app.route('/api', subApp)和app.basePath('/api')都能加前缀,但内部机制不同- 多层
app.route()可以嵌套,路径前缀逐层叠加 - 路由分组的注册顺序决定匹配优先级,先注册的挂载点先被检查
- 父 app 的中间件向下穿透到所有子 app,Context 在整个挂载链中共享
- 按功能模块分组比按技术层分组更适合大多数项目的演进节奏
- 同一挂载路径可以注册多个不同 HTTP 方法的子 app,互不冲突
1. 什么是路由分组
路由超过十来个之后,全部写在同一个文件里会变得难以维护。更实际的问题在于:相关的路由之间通常共享业务逻辑,但和其他路由没有关系。把用户管理和文章管理的路由混在一起,既不方便查找,也不方便独立测试。
路由分组要解决的问题:把逻辑上相关的路由归到一组,给它们统一的路径前缀,同时让每个分组可以独立管理自己的路由和中间件。
Hono 提供两种分组手段:
app.route(path, subApp)— 将子 Hono 实例挂载到指定路径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/usersapp.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 ← 处理函数
中间件穿透规则:
- 父 app 的
app.use()中间件对所有子 app 生效 - 子 app 内部中间件只对该子 app 的路由生效
- 执行顺序从外到内:父 app 中间件先执行,子 app 后执行
- 响应阶段反向
同一个中间件既挂父 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,挂载到同一个路径 /users。app.use('/users/*', authMiddleware) 写在读操作路由之后、写操作路由之前,所以只对写操作生效。
这个模式在需要为不同操作设置不同中间件策略时有用。如果不需要区分中间件,直接在同一个子 app 里注册所有方法即可。
延伸阅读
- 01-基础路由 —
app.route()基本用法和路由优先级规则 - 03.04-Context 上下文对象原理 — Context 在路由挂载链中的传递机制
- 03.02-路由匹配原理 — 路由器如何在内部处理路径匹配
- Hono Routing 文档 —
app.route()的 API 参考
总结
路由分组是 Hono 组织大型应用的基础手段。app.route() 支持独立子 app 和多层嵌套,basePath() 更适合扁平的统一前缀场景。多层嵌套时路径前缀逐层叠加,子 app 内部路径应相对于挂载点来写。注册顺序影响匹配优先级——挂载路径有包含关系时,更具体的前缀先注册。中间件从父 app 向下穿透,Context 在整个链路中共享。不同规模项目适合不同的分组模式,从小型的扁平路由到大型的多层分组。
下一篇(06-模块化路由拆分)继续讨论如何把路由模块进一步拆到独立的目录结构中,以及如何处理模块间的中间件共享和类型传递。