中间件

要点

  • Hono 的中间件长这样
  • 如果你用过 Koa,这个概念不陌生
  • 一个实用的中间件——记录每个请求花了多长时间
  • 检查请求头里有没有合法的 API Key
  • Hono 自带了一批开箱即用的中间件,不用自己造轮子

内容

1. 中间件的基本形式

Hono 的中间件长这样:

// index.ts
import { Hono } from 'hono'
 
const app = new Hono()
 
app.use('*', async (c, next) => {
 
  // 请求到达路由之前,做点事情
 
  console.log('请求进来了')
 
  await next() // 把请求交给下一个中间件或路由处理
 
  // 路由处理完了,响应出去之前,做点事情
 
  console.log('响应出去了')
 
})
 
app.get('/', (c) => {
 
  return c.text('Hello!')
 
})
 
export default app

核心就是 await next()。它把中间件分成两半:next() 之前处理请求,next() 之后处理下游中间件或路由返回后的阶段。

你也可以把它理解成一个分叉点:

  • 调用 await next():请求继续往后走,交给下一个中间件或最终路由
  • 直接 return c.json(...) / return c.text(...):当前中间件就把请求结束掉,后面的逻辑不再执行

鉴权、权限判断、参数拦截,本质上都是在这里决定"放行"还是"拦截"。

2. 洋葱模型

如果你用过 Koa,这个概念不陌生。多个中间件的执行顺序像剥洋葱:

// index.ts
app.use('*', async (c, next) => {
 
  console.log('中间件 A - 进')
 
  await next()
 
  console.log('中间件 A - 出')
 
})
 
app.use('*', async (c, next) => {
 
  console.log('中间件 B - 进')
 
  await next()
 
  console.log('中间件 B - 出')
 
})
 
app.get('/', (c) => {
 
  console.log('路由处理')
 
  return c.text('Hello!')
 
})

控制台输出:

// output.txt
中间件 A - 进
 
中间件 B - 进
 
路由处理
 
中间件 B - 出
 
中间件 A - 出

请求从外到内穿过中间件,响应从内到外返回。先注册的中间件最先接触请求、最后接触返回阶段。

3. 自定义中间件:请求计时

一个实用的中间件——记录每个请求花了多长时间:

// index.ts
import { Hono } from 'hono'
 
const app = new Hono()
 
// 请求计时中间件
 
app.use('*', async (c, next) => {
 
  const start = Date.now()
 
  await next()
 
  const duration = Date.now() - start
 
  console.log(`${c.req.method} ${c.req.path} - ${duration}ms`)
 
})
 
app.get('/', (c) => {
 
  return c.text('Hello!')
 
})
 
export default app

await next() 前记录开始时间,await next() 后计算耗时。洋葱模型的经典用法。

4. 自定义中间件:简单鉴权

检查请求头里有没有合法的 API Key:

// index.ts
import { Hono } from 'hono'
 
const app = new Hono()
 
// 鉴权中间件
 
// 这里为了先讲清概念,先省略 Hono 的类型声明
 
const authMiddleware = async (c, next) => {
 
  const apiKey = c.req.header('X-API-Key')
 
  if (apiKey !== 'my-secret-key') {
 
    return c.json({ error: 'Unauthorized' }, 401)
 
  }
 
  await next()
 
}
 
// 公开接口,不需要鉴权
 
app.get('/', (c) => {
 
  return c.text('Public')
 
})
 
// 需要鉴权的接口
 
app.get('/api/secret', authMiddleware, (c) => {
 
  return c.json({ data: 'This is secret' })
 
})
 
export default app

注意:如果鉴权失败,直接 return c.json(...) 就行,不调用 next(),请求就不会继续往下走。

如果你在 TypeScript 严格模式里写项目,后面最好把这类中间件补上类型,或者直接使用 Hono 提供的 createMiddleware() 来创建中间件。教程这里先把注意力放在执行流程本身。

5. 内置中间件

Hono 自带了一批开箱即用的中间件,不用自己造轮子:

// index.ts
import { Hono } from 'hono'
 
import { cors } from 'hono/cors'
 
import { logger } from 'hono/logger'
 
import { timing } from 'hono/timing'
 
import { prettyJSON } from 'hono/pretty-json'
 
import { secureHeaders } from 'hono/secure-headers'
 
const app = new Hono()
 
// 请求日志:控制台打印每个请求的方法、路径、状态码、耗时
 
app.use('*', logger())
 
// 跨域配置:允许前端跨域访问
 
app.use('*', cors({
 
  origin: 'http://localhost:3000',
 
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
 
}))
 
// Server-Timing 头:响应头里带上各阶段耗时,浏览器 DevTools 可以看到
 
app.use('*', timing())
 
// 美化 JSON:请求 URL 加 ?pretty 参数时,返回格式化的 JSON
 
app.use('*', prettyJSON())
 
// 安全响应头:自动设置 X-Frame-Options、X-Content-Type-Options 等
 
app.use('*', secureHeaders())
 
app.get('/api/users', (c) => {
 
  return c.json([
 
    { id: 1, name: 'Alice' },
 
    { id: 2, name: 'Bob' },
 
  ])
 
})
 
export default app

每个中间件干的事情:

  • logger():控制台打印 <-- GET /api/users--> GET /api/users 200 12ms
  • cors():处理跨域预检请求(OPTIONS),设置 Access-Control-Allow-* 响应头
  • timing():在响应头加 Server-Timing 字段,方便性能分析
  • prettyJSON():访问 /api/users?pretty 时返回缩进后的 JSON
  • secureHeaders():一键设置安全相关的 HTTP 头,防 XSS、点击劫持等

6. 中间件挂载范围

中间件可以作用在不同范围:

// index.ts
import { Hono } from 'hono'
 
import { logger } from 'hono/logger'
 
import { cors } from 'hono/cors'
 
const app = new Hono()
 
// 全局中间件:所有路由都会经过
 
app.use('*', logger())
 
// 路径级中间件:只有 /api 开头的路由会经过
 
app.use('/api/*', cors())
 
// 鉴权中间件,只用在 /api 下
 
const auth = async (c, next) => {
 
  const token = c.req.header('Authorization')
 
  if (!token) {
 
    return c.json({ error: 'Unauthorized' }, 401)
 
  }
 
  await next()
 
}
 
app.use('/api/*', auth)
 
// 单个路由级别:把中间件直接写在路由参数里
 
const adminOnly = async (c, next) => {
 
  const role = c.req.header('X-Role')
 
  if (role !== 'admin') {
 
    return c.json({ error: 'Forbidden' }, 403)
 
  }
 
  await next()
 
}
 
app.delete('/api/users/:id', adminOnly, (c) => {
 
  const id = c.req.param('id')
 
  return c.json({ message: `User ${id} deleted` })
 
})
 
// 公开路由,不受 /api/* 的中间件影响
 
app.get('/', (c) => {
 
  return c.text('Public homepage')
 
})
 
export default app

三种粒度:

  • app.use('*', ...):全局,所有请求都过
  • app.use('/api/*', ...):路径前缀匹配,只有 /api/ 下的请求会过
  • app.get('/path', middleware, handler):只对这一个路由生效

7. 执行顺序很重要

中间件按注册顺序执行。顺序搞错了会出问题:

// index.ts
import { Hono } from 'hono'
 
import { cors } from 'hono/cors'
 
import { logger } from 'hono/logger'
 
const app = new Hono()
 
// 正确顺序:cors 在鉴权之前
 
// 因为浏览器跨域会先发 OPTIONS 预检请求
 
// 如果鉴权在 cors 之前,预检请求没带 token,直接被 401 了
 
app.use('*', logger())
 
app.use('*', cors())
 
app.use('/api/*', async (c, next) => {
 
  const token = c.req.header('Authorization')
 
  if (!token) {
 
    return c.json({ error: 'Unauthorized' }, 401)
 
  }
 
  await next()
 
})
 
app.get('/api/data', (c) => {
 
  return c.json({ message: 'Protected data' })
 
})
 
export default app

一般推荐的顺序:loggercorssecureHeaders → 鉴权 → 业务逻辑。日志最先,这样所有请求(包括被拦截的)都能被记录到。

8. 用 c.set / c.get 在中间件和路由之间传数据

中间件处理完的结果,怎么传给后面的路由?用 c.set()c.get()

// index.ts
import { Hono } from 'hono'
 
const app = new Hono()
 
// 鉴权中间件:解析 token,把用户信息存到 context 里
 
app.use('/api/*', async (c, next) => {
 
  const token = c.req.header('Authorization')
 
  if (!token) {
 
    return c.json({ error: 'Unauthorized' }, 401)
 
  }
 
  // 假设解析 token 得到了用户信息
 
  const user = { id: 1, name: 'Alice', role: 'admin' }
 
  // 把用户信息存到 context 里
 
  c.set('user', user)
 
  await next()
 
})
 
// 路由里通过 c.get() 拿到用户信息
 
app.get('/api/profile', (c) => {
 
  const user = c.get('user')
 
  return c.json({ user })
 
})
 
app.get('/api/admin', (c) => {
 
  const user = c.get('user')
 
  if (user.role !== 'admin') {
 
    return c.json({ error: 'Forbidden' }, 403)
 
  }
 
  return c.json({ message: 'Welcome, admin' })
 
})
 
export default app

c.set() / c.get() 是中间件与路由之间的通信通道。数据只在当前请求的生命周期内有效,不同请求之间互不影响。

和前一篇一样,这里为了先理解机制,先省略了类型声明。真实项目里,像 user 这种在中间件里写入、在路由里读取的数据,最好补上类型,不然编辑器很难准确提示字段。

9. 总结

中间件是 Hono 处理横切关注点的核心机制。洋葱模型让你可以在请求前后都插入逻辑,c.set() / c.get() 解决了中间件和路由之间的数据传递。内置中间件覆盖了大部分常见需求,自定义中间件也就是一个 async 函数的事。

下一篇看数据校验——怎么优雅地验证请求参数和请求体。