路由中间件

要点

  • 路由中间件通过路径前缀 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 app

auth 先执行,把用户信息写入 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 时的完整链路:

  1. 主应用的 /api/* 鉴权中间件
  2. 子应用的 /admin/* 角色校验中间件
  3. /admin/users 路由处理

访问 /api/public/status 时的链路:

  1. 主应用的 /api/* 鉴权中间件
  2. /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

路由文件只关心「这条路由需要哪些中间件」,中间件的具体实现集中维护。新增一条路由或调整中间件策略时,影响范围可控。

总结

路由中间件是介于全局中间件和单路由处理之间的粒度,用于处理「与路径相关、但又不属于单一路由」的横切逻辑。

这一节涉及到的几种注册方式:

  1. 路径前缀app.use('/api/*', ...),匹配一组路由
  2. 路由内联app.get('/path', mw1, mw2, handler),只匹配一条路由
  3. 子应用挂载app.route('/api', subApp),子应用的中间件只对子路由生效
  4. 多中间件叠加:按书写顺序执行,先注册的先接触请求

路径匹配的规则与 Hono 路由一致,需要注意尾部斜杠、参数段和通配符层级的差异。

组织上,中大型项目倾向于把中间件抽到独立文件,路由文件只做引用。这种结构让中间件策略的调整和路由变更互相独立。

下一篇看中间件的执行顺序——同一个请求经过多个中间件时,顺序不同会带来哪些实际差异。