中间件原理:洋葱模型
Hono 的中间件采用"洋葱模型"(Onion Model)——请求像剥洋葱一样从外层向内层依次执行,响应再从内层向外层依次执行。每个中间件都可以在 next() 前后执行逻辑:
// 中间件结构:async (c, next) => { ... await next() ... }
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')
})
// 请求 GET / 的执行顺序:
// [中间件A] 进入
// [中间件B] 进入
// [处理函数] 执行
// [中间件B] 返回
// [中间件A] 返回
洋葱模型的强大之处:中间件可以在请求进入时预处理(鉴权、日志开始),也可以在响应离开时后处理(添加响应头、记录耗时、压缩响应体)。这种双向处理能力是其他"单向"中间件模型(如 Express 的 next 链)所不具备的。
内置中间件
Logger — 请求日志
import { Hono } from 'hono'
import { logger } from 'hono/logger'
const app = new Hono()
app.use(logger()) // 全局日志
// 输出格式:
// --> GET /api/users
// <-- GET /api/users 200 12ms
CORS — 跨域资源共享
import { cors } from 'hono/cors'
// 简单配置(允许所有来源,不推荐用于生产)
app.use(cors())
// 完整配置
app.use(cors({
origin: ['https://myapp.com', 'https://staging.myapp.com'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
exposeHeaders: ['X-Total-Count'],
credentials: true, // 允许携带 Cookie
maxAge: 3600, // OPTIONS 预检缓存 1 小时
}))
// 仅对 /api/* 路径启用 CORS
app.use('/api/*', cors({ origin: '*' }))
Compress — 响应压缩
import { compress } from 'hono/compress'
// 自动根据 Accept-Encoding 选择 gzip 或 deflate
app.use(compress())
// 指定编码方式
app.use(compress({ encoding: 'gzip' }))
ETag — 条件请求缓存
import { etag } from 'hono/etag'
app.use(etag())
// 自动计算响应体的 ETag,支持 If-None-Match 条件请求
// 内容未变时返回 304 Not Modified,节省带宽
Basic Auth / Bearer Auth
import { basicAuth } from 'hono/basic-auth'
import { bearerAuth } from 'hono/bearer-auth'
// HTTP Basic 认证
app.use('/admin/*', basicAuth({
username: 'admin',
password: process.env.ADMIN_PASSWORD ?? 'secret',
}))
// Bearer Token 认证
app.use('/api/*', bearerAuth({
token: process.env.API_TOKEN ?? 'my-secret-token',
}))
// 要求 Header: Authorization: Bearer <token>
Cache — 边缘缓存
import { cache } from 'hono/cache'
// 在 Cloudflare Workers 上使用 Cache API
app.use('/static/*', cache({
cacheName: 'my-app-cache',
cacheControl: 'max-age=3600', // 缓存 1 小时
}))
全局 vs 路由级中间件
// 全局中间件:对所有路由生效
app.use(logger())
app.use(cors())
// 路由级中间件:仅对匹配路径生效
app.use('/api/*', bearerAuth({ token: 'secret' }))
// 单个路由的内联中间件
app.get('/sensitive', authMiddleware, rateLimitMiddleware, (c) => {
return c.json({ data: 'sensitive data' })
})
自定义中间件
请求响应时间记录
// middleware/timing.ts
import { type MiddlewareHandler } from 'hono'
export const timingMiddleware: MiddlewareHandler = async (c, next) => {
const start = Date.now()
await next()
const ms = Date.now() - start
c.header('X-Response-Time', `${ms}ms`)
console.log(`${c.req.method} ${c.req.path} → ${c.res.status} [${ms}ms]`)
}
app.use(timingMiddleware)
用户鉴权中间件
// middleware/auth.ts
import { type MiddlewareHandler } from 'hono'
// 定义类型扩展(让 c.get('user') 有正确类型)
type Variables = {
user: { id: string; email: string }
}
export const authMiddleware: MiddlewareHandler<{ Variables: Variables }> = async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return c.json({ error: 'Unauthorized' }, 401)
}
try {
// 验证 token(简化示例)
const user = await verifyToken(token)
c.set('user', user) // 将用户信息传递给后续处理函数
await next()
} catch {
return c.json({ error: 'Invalid token' }, 401)
}
}
createMiddleware 工厂函数
当中间件需要接受配置参数时,使用 createMiddleware 创建带参数的中间件工厂:
import { createMiddleware } from 'hono/factory'
// 带参数的限流中间件
export const rateLimit = (options: {
windowMs: number
max: number
}) => createMiddleware(async (c, next) => {
const ip = c.req.header('CF-Connecting-IP') ?? 'unknown'
const key = `rate_limit:${ip}`
// 这里简化了限流逻辑,实际应使用 KV/Redis
const count = requestCounts.get(key) ?? 0
if (count >= options.max) {
return c.json(
{ error: 'Too many requests' },
429,
{ 'Retry-After': String(options.windowMs / 1000) }
)
}
requestCounts.set(key, count + 1)
await next()
})
// 使用
app.use('/api/*', rateLimit({ windowMs: 60_000, max: 100 }))
中间件顺序很重要:app.use() 的调用顺序决定了中间件的执行顺序。通常推荐:logger → cors → compress → 自定义安全中间件 → 路由处理。在注册路由之前调用 app.use(),否则中间件不会对已注册的路由生效。