路由中间件
要点
- 路由中间件通过路径前缀
app.use('/api/*', ...)或路由内联app.get('/path', mw, handler)注册 - 路径前缀匹配影响的是「这一组路由」,内联中间件影响的是「这一条路由」
app.route()挂载子应用后,子应用内的中间件只对子路由生效- 路由中间件常用于鉴权、角色校验、请求体解析、限流等与路径相关的横切逻辑
- 中间件可以叠加多个,按书写顺序执行
- 路径匹配规则和 Hono 的路由匹配一致:精确路径、
:param参数段、*通配符
1. 路径前缀中间件
前缀中间件的写法是把 '*' 换成具体的路径模式:
// src/index.ts
import { Hono } from 'hono'
const app = new Hono()
// 只对 /api 开头的路径生效
app.use('/api/*', async (c, next) => {
const token = c.req.header('Authorization')
if (!token) {
return c.json({ error: 'Unauthorized' }, 401)
}
// 验证 token(略)
await next()
})
// 公开路由,不经过上面的鉴权中间件
app.get('/', (c) => c.text('Public homepage'))
app.get('/health', (c) => c.json({ status: 'ok' }))
// API 路由,会经过鉴权中间件
app.get('/api/users', (c) => c.json([{ id: 1, name: 'Alice' }]))
app.get('/api/posts', (c) => c.json([]))
export default app'/api/*' 匹配所有以 /api/ 开头的路径。访问 / 或 /health 时,这个鉴权中间件不会被执行。
路径模式支持几种写法:
'/api/*':匹配/api/及其所有子路径'/api/users/:id':精确匹配参数段,例如/api/users/42'/api/*':匹配/api/下的任意层级,例如/api/v1/users/42/posts
前缀中间件的作用范围是「符合条件的所有路由」,它的粒度介于全局中间件和单路由中间件之间。
2. 单路由内联中间件
如果中间件只对某一条路由生效,可以直接写在路由声明里:
// src/index.ts
import { Hono } from 'hono'
const app = new Hono()
// 角色校验中间件:只作用于删除用户这一条路由
const adminOnly = async (c, next) => {
const user = c.get('user') as { role: string } | undefined
if (!user || user.role !== 'admin') {
return c.json({ error: 'Forbidden' }, 403)
}
await next()
}
// 鉴权中间件:把解析出的用户信息存到 context
const auth = async (c, next) => {
const token = c.req.header('Authorization')
if (!token) {
return c.json({ error: 'Unauthorized' }, 401)
}
// 假设解析 token 得到用户信息
c.set('user', { id: 1, name: 'Alice', role: 'admin' })
await next()
}
// 路由内联多个中间件,按书写顺序执行
app.delete('/api/users/:id', auth, adminOnly, (c) => {
const id = c.req.param('id')
return c.json({ message: `User ${id} deleted` })
})
export default appauth 先执行,把用户信息写入 context;adminOnly 再执行,读取 context 里的用户信息做角色判断。两个中间件 + 最终的路由处理函数,串成一条完整的处理链路。
内联中间件的好处是作用域非常清晰——看到路由声明就知道它挂了哪些中间件。缺点是同一个中间件如果被多条路由共用,每条路由都要写一次。
3. 多中间件叠加
中间件可以叠加多个,执行顺序按书写位置从前到后。这个规则在内联和前缀两种写法里都适用:
// src/index.ts
import { Hono } from 'hono'
const app = new Hono()
// 前缀中间件:/api 下所有路由先经过鉴权
app.use('/api/*', async (c, next) => {
console.log('1. auth - 前')
const token = c.req.header('Authorization')
if (token) {
c.set('user', { id: 1, role: 'admin' })
}
await next()
console.log('1. auth - 后')
})
// 路由内联:再经过日志、角色校验
const logRoute = async (c, next) => {
console.log('2. logRoute - 前')
await next()
console.log('2. logRoute - 后')
}
const adminOnly = async (c, next) => {
console.log('3. adminOnly - 前')
const user = c.get('user') as { role: string } | undefined
if (user?.role !== 'admin') {
return c.json({ error: 'Forbidden' }, 403)
}
await next()
console.log('3. adminOnly - 后')
}
app.delete('/api/users/:id', logRoute, adminOnly, (c) => {
console.log('4. handler')
return c.json({ ok: true })
})
export default app控制台输出的顺序:
// output.txt
1. auth - 前
2. logRoute - 前
3. adminOnly - 前
4. handler
3. adminOnly - 后
2. logRoute - 后
1. auth - 后前缀中间件最先执行,然后是按书写顺序进入内联中间件,最后到达路由处理函数。返回阶段按相反顺序回溯。
4. 子应用挂载与中间件继承
app.route() 挂载子应用时,子应用内部的中间件只对子路由生效。主应用的前缀中间件和子应用的中间件会叠加:
// src/apps/api.ts
import { Hono } from 'hono'
const api = new Hono()
// 子应用的前缀中间件
api.use('/admin/*', async (c, next) => {
const user = c.get('user') as { role: string } | undefined
if (user?.role !== 'admin') {
return c.json({ error: 'Forbidden' }, 403)
}
await next()
})
api.get('/admin/users', (c) => c.json([]))
api.get('/public/status', (c) => c.json({ ok: true }))
export default api// src/index.ts
import { Hono } from 'hono'
import api from './apps/api'
const app = new Hono()
// 主应用的鉴权中间件
app.use('/api/*', async (c, next) => {
const token = c.req.header('Authorization')
if (!token) {
return c.json({ error: 'Unauthorized' }, 401)
}
c.set('user', { id: 1, role: 'admin' })
await next()
})
app.route('/api', api)
export default app访问 /api/admin/users 时的完整链路:
- 主应用的
/api/*鉴权中间件 - 子应用的
/admin/*角色校验中间件 /admin/users路由处理
访问 /api/public/status 时的链路:
- 主应用的
/api/*鉴权中间件 /public/status路由处理(子应用的/admin/*中间件不匹配,跳过)
这种分层叠加的方式,让权限模型可以按模块组织:主应用负责通用的 token 校验,子应用各自决定是否需要角色判断、限流或其他业务前置条件。
5. 请求体解析中间件
请求体解析是路由中间件的另一个典型场景。不同路由接受的请求体格式可能不同,按路径挂载比全局挂载更合理:
// src/index.ts
import { Hono } from 'hono'
const app = new Hono()
// JSON body 解析:只对 POST/PUT/PATCH 的 API 路由生效
app.use('/api/*', async (c, next) => {
// 只有带 Content-Type: application/json 的请求才解析
if (['POST', 'PUT', 'PATCH'].includes(c.req.method)
&& c.req.header('content-type')?.includes('application/json')) {
try {
const body = await c.req.json()
c.set('parsedBody', body)
} catch {
return c.json({ error: 'Invalid JSON' }, 400)
}
}
await next()
})
app.post('/api/users', (c) => {
const body = c.get('parsedBody') as { name: string }
return c.json({ id: 1, name: body.name }, 201)
})
app.get('/api/users', (c) => {
// GET 请求不经过 body 解析
return c.json([])
})
export default app也可以在路由里直接调用 c.req.json() 解析 body,不需要中间件。中间件方式的优势是把解析和错误处理集中到一处,路由处理函数只需要 c.get('parsedBody')。
6. 路径匹配的细节
路径前缀中间件的匹配规则和 Hono 路由一致,有几个容易混淆的地方:
6.1 尾部斜杠
'/api/*' 匹配 /api/users,但不匹配 /api 本身。如果希望 /api 也命中,需要额外注册一条:
app.use('/api', handler) // 匹配 /api
app.use('/api/*', handler) // 匹配 /api/users、/api/users/1 等6.2 参数段
中间件路径里也可以用 :param:
app.use('/users/:id/profile', async (c, next) => {
const id = c.req.param('id')
// 做权限校验
await next()
})不过中间件路径里的 :param 只在中间件内部可读,不会传给路由。路由路径里的 :param 才是最终暴露给路由处理函数的参数。
6.3 通配符的层级
'/api/*' 匹配 /api/ 下的任意层级,包括 /api/a/b/c/d。如果只想匹配一级子路径,需要写成 '/api/:segment':
// 只匹配 /api/users、/api/posts,不匹配 /api/users/1/posts
app.use('/api/:segment', async (c, next) => {
await next()
})7. 路由中间件的常见组织方式
中大型项目里,路由中间件通常按职责抽到独立文件,避免路由文件里出现大段 inline 逻辑:
// directory.txt
src/
├── middleware/
│ ├── auth.ts ← 鉴权中间件
│ ├── admin.ts ← 角色校验中间件
│ ├── body-parser.ts ← 请求体解析
│ └── rate-limit.ts ← 限流中间件
├── routes/
│ ├── users.ts ← 用户路由
│ └── posts.ts ← 文章路由
└── index.ts// src/routes/users.ts
import { Hono } from 'hono'
import { auth } from '../middleware/auth'
import { adminOnly } from '../middleware/admin'
const users = new Hono()
users.get('/', (c) => c.json([]))
users.post('/', auth, async (c) => { /* 创建用户 */ })
users.delete('/:id', auth, adminOnly, (c) => { /* 删除用户 */ })
export default users// src/index.ts
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import users from './routes/users'
const app = new Hono()
app.use('*', logger())
app.route('/users', users)
export default app路由文件只关心「这条路由需要哪些中间件」,中间件的具体实现集中维护。新增一条路由或调整中间件策略时,影响范围可控。
总结
路由中间件是介于全局中间件和单路由处理之间的粒度,用于处理「与路径相关、但又不属于单一路由」的横切逻辑。
这一节涉及到的几种注册方式:
- 路径前缀:
app.use('/api/*', ...),匹配一组路由 - 路由内联:
app.get('/path', mw1, mw2, handler),只匹配一条路由 - 子应用挂载:
app.route('/api', subApp),子应用的中间件只对子路由生效 - 多中间件叠加:按书写顺序执行,先注册的先接触请求
路径匹配的规则与 Hono 路由一致,需要注意尾部斜杠、参数段和通配符层级的差异。
组织上,中大型项目倾向于把中间件抽到独立文件,路由文件只做引用。这种结构让中间件策略的调整和路由变更互相独立。
下一篇看中间件的执行顺序——同一个请求经过多个中间件时,顺序不同会带来哪些实际差异。