自定义中间件开发

要点

  • 中间件本质上是一个 async (c, next) => void 函数
  • createMiddleware() 提供类型推导,减少手动标注类型的成本
  • c.set() / c.get() 是中间件向下游传递数据的通道
  • 中间件可以接收配置参数,通过闭包返回真正的处理函数
  • 类型安全的 c.set() / c.get() 需要扩展 ContextVariableMap
  • 中间件可以抽到独立文件,按职责组织到 src/middleware/ 目录

1. 最简单的中间件

中间件就是一个 async 函数,接收 context 和 next 两个参数:

// src/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
  c.header('X-Response-Time', `${duration}ms`)
})
 
app.get('/', (c) => c.text('Hello'))
 
export default app

c.header() 在响应里添加自定义头。await next() 之前的代码在请求到达路由前执行,之后的代码在路由返回后执行。

这种内联写法适合一次性的逻辑。如果同一个中间件需要在多处使用,应该把它抽成独立函数。

2. 抽成独立函数

把中间件函数赋给一个变量,就可以在任何地方复用:

// src/middleware/timing.ts
import type { Context, Next } from 'hono'
 
export const timing = async (c: Context, next: Next) => {
  const start = Date.now()
  await next()
  const duration = Date.now() - start
  c.header('X-Response-Time', `${duration}ms`)
}
// src/index.ts
import { Hono } from 'hono'
import { timing } from './middleware/timing'
 
const app = new Hono()
app.use('*', timing)
app.get('/', (c) => c.text('Hello'))
 
export default app

类型标注 ContextNext 来自 hono。这种写法已经可以工作,但 c.get() / c.set() 的键值类型都是 any,缺少类型保护。

3. createMiddleware 工厂函数

Hono 提供了 createMiddleware() 工厂函数,用于创建类型安全的中间件:

// src/middleware/timing.ts
import { createMiddleware } from 'hono/factory'
 
export const timing = createMiddleware(async (c, next) => {
  const start = Date.now()
  await next()
  const duration = Date.now() - start
  c.header('X-Response-Time', `${duration}ms`)
})

createMiddleware 自动推导 cnext 的类型,不需要手动标注。

如果中间件需要接收配置参数,用外层函数返回 createMiddleware 的结果:

// src/middleware/timing.ts
import { createMiddleware } from 'hono/factory'
 
type TimingOptions = {
  headerName?: string
}
 
export const timing = (options: TimingOptions = {}) => {
  const headerName = options.headerName ?? 'X-Response-Time'
 
  return createMiddleware(async (c, next) => {
    const start = Date.now()
    await next()
    const duration = Date.now() - start
    c.header(headerName, `${duration}ms`)
  })
}
// src/index.ts
import { Hono } from 'hono'
import { timing } from './middleware/timing'
 
const app = new Hono()
 
// 使用默认配置
app.use('*', timing())
 
// 使用自定义配置
app.use('/api/*', timing({ headerName: 'X-Api-Response-Time' }))
 
export default app

这种「接收配置 → 返回中间件」的模式在 Hono 内置中间件里广泛使用,例如 cors()logger()secureHeaders() 都是这个形状。

4. 中间件之间的数据传递

中间件处理完的结果需要传给下游路由时,用 c.set() 写入、c.get() 读取:

// src/middleware/auth.ts
import { createMiddleware } from 'hono/factory'
 
type User = {
  id: string
  name: string
  role: string
}
 
export const auth = createMiddleware(async (c, next) => {
  const token = c.req.header('Authorization')?.replace('Bearer ', '')
 
  if (!token) {
    return c.json({ error: 'Unauthorized' }, 401)
  }
 
  // 假设 token 解析后得到用户信息
  const user: User = { id: '1', name: 'Alice', role: 'admin' }
 
  // 把用户信息存入 context
  c.set('user', user)
 
  await next()
})
// src/index.ts
import { Hono } from 'hono'
import { auth } from './middleware/auth'
 
const app = new Hono()
 
app.get('/api/profile', auth, (c) => {
  // 从 context 读取用户信息
  const user = c.get('user') as { id: string; name: string; role: string }
  return c.json({ user })
})
 
export default app

直接写 c.get('user') 返回的类型是 any,需要手动 as 转换。这种写法能工作,但类型保护很弱——拼错键名、字段名都不会在编译期报错。

5. 类型安全的 c.set / c.get

要让 c.set() / c.get() 具备类型推导,需要扩展 Hono 的 ContextVariableMap

// src/types/hono.ts
import 'hono'
 
type User = {
  id: string
  name: string
  role: string
}
 
declare module 'hono' {
  interface ContextVariableMap {
    user: User
    requestId: string
  }
}

扩展之后,c.get('user') 的返回类型自动推导为 Userc.get('requestId') 推导为 string

// src/index.ts
import { Hono } from 'hono'
import { auth } from './middleware/auth'
import './types/hono'  // 确保类型扩展被加载
 
const app = new Hono()
 
app.get('/api/profile', auth, (c) => {
  // 类型自动推导为 User
  const user = c.get('user')
  // user.id、user.name、user.role 都有类型提示
  return c.json({ user })
})
 
export default app

拼错键名会在编译期报错:

const u = c.get('usr')  // ❌ 类型错误:'usr' 不存在于 ContextVariableMap

这种类型扩展方式是 Hono 的内置设计,不是 as any 的 workaround。ContextVariableMap 是一个开放的接口,任何模块都可以通过 declare module 'hono' 来扩展它。

6. 带类型约束的中间件

有些中间件需要在特定的 context 类型下才能使用。例如 auth 中间件写入的 user 字段,只有经过鉴权的路由才能读取。

可以用泛型约束来编码这种规则:

// src/middleware/auth.ts
import { createMiddleware } from 'hono/factory'
import type { Context } from 'hono'
 
type User = {
  id: string
  name: string
  role: string
}
 
type Env = {
  Variables: {
    user: User
  }
}
 
export const auth = createMiddleware<Env>(async (c, next) => {
  const token = c.req.header('Authorization')?.replace('Bearer ', '')
  if (!token) {
    return c.json({ error: 'Unauthorized' }, 401)
  }
 
  const user: User = { id: '1', name: 'Alice', role: 'admin' }
  c.set('user', user)
  await next()
})
// src/index.ts
import { Hono } from 'hono'
import { auth } from './middleware/auth'
 
// 路由声明 Env 类型后,c.get('user') 自动推导为 User
const app = new Hono<{ Variables: { user: { id: string; name: string; role: string } } }>()
 
app.get('/api/profile', auth, (c) => {
  const user = c.get('user')  // 类型:{ id: string; name: string; role: string }
  return c.json({ user })
})
 
export default app

这种方式比全局扩展 ContextVariableMap 更精确,适合不同模块有不同 context 变量的场景。缺点是类型声明比较长,代码可读性会降低。

两种方式的取舍:

  • ContextVariableMap 扩展:适合全局共享的变量(user、requestId、locale 等),声明一次,全局可用
  • 泛型 Env:适合模块特有的变量,类型隔离更严格,但需要在每个 app 实例上声明

7. 中间件的错误处理

中间件里的错误如果没被捕获,会冒泡到 Hono 的全局错误处理。可以在中间件内部用 try-catch 处理:

// src/middleware/auth.ts
import { createMiddleware } from 'hono/factory'
 
export const auth = createMiddleware(async (c, next) => {
  try {
    const token = c.req.header('Authorization')?.replace('Bearer ', '')
    if (!token) {
      return c.json({ error: 'Unauthorized' }, 401)
    }
 
    // 假设 token 解析可能抛异常
    const user = await verifyToken(token)
    c.set('user', user)
    await next()
  } catch (err) {
    // 记录日志
    console.error('Auth failed:', err)
    // 返回统一格式的错误响应
    return c.json({ error: 'Invalid token' }, 401)
  }
})

也可以不在中间件里处理错误,让错误冒泡到 app.onError()

// src/index.ts
import { Hono } from 'hono'
 
const app = new Hono()
 
app.onError((err, c) => {
  console.error('Unhandled error:', err)
  return c.json({ error: 'Internal Server Error' }, 500)
})

两种方式的选择:

  • 中间件内部 try-catch:适合预期内的错误(token 无效、参数缺失),可以返回精确的状态码和错误信息
  • 冒泡到 onError:适合非预期的异常(网络超时、数据库断开),统一返回 500

中间件里「预期会失败」的场景建议就地处理,避免错误冒泡到全局后才丢失上下文。

8. 中间件的测试

自定义中间件可以独立测试。创建一个 mini-app,只挂载要测试的中间件和一个捕获路由:

// tests/middleware/timing.test.ts
import { describe, it, expect } from 'vitest'
import { Hono } from 'hono'
import { timing } from '../../src/middleware/timing'
 
describe('timing middleware', () => {
  it('在响应头写入 X-Response-Time', async () => {
    const app = new Hono()
    app.use('*', timing())
    app.get('/', (c) => c.text('ok'))
 
    const res = await app.request('/')
    expect(res.headers.get('X-Response-Time')).toMatch(/\d+ms/)
  })
 
  it('支持自定义 header name', async () => {
    const app = new Hono()
    app.use('*', timing({ headerName: 'X-Custom-Time' }))
    app.get('/', (c) => c.text('ok'))
 
    const res = await app.request('/')
    expect(res.headers.get('X-Custom-Time')).toMatch(/\d+ms/)
    expect(res.headers.get('X-Response-Time')).toBeNull()
  })
})

测试 c.set() / c.get() 的中间件时,在捕获路由里读取 context 变量并返回:

// tests/middleware/auth.test.ts
import { describe, it, expect } from 'vitest'
import { Hono } from 'hono'
import { auth } from '../../src/middleware/auth'
 
describe('auth middleware', () => {
  it('token 有效时放行并设置 user', async () => {
    const app = new Hono()
    app.use('*', auth)
    app.get('/', (c) => c.json({ user: c.get('user') }))
 
    const res = await app.request('/', {
      headers: { Authorization: 'Bearer valid-token' },
    })
    expect(res.status).toBe(200)
    expect(await res.json()).toHaveProperty('user')
  })
 
  it('token 缺失时返回 401', async () => {
    const app = new Hono()
    app.use('*', auth)
    app.get('/', (c) => c.text('ok'))
 
    const res = await app.request('/')
    expect(res.status).toBe(401)
  })
})

总结

自定义中间件的开发路径从内联函数开始,逐步演进到 createMiddleware() 工厂、类型扩展和独立测试。

这一节涉及到的几个层次:

  1. 基本形式async (c, next) => void 函数,await next() 前后分别处理请求和响应
  2. 工厂模式:外层函数接收配置,内层返回 createMiddleware() 的结果,例如 timing(&#123; headerName: '...' &#125;)
  3. 数据传递c.set() / c.get() 在中间件和路由之间传递数据
  4. 类型安全:两种方式——扩展 ContextVariableMap 适合全局变量,泛型 Env 适合模块变量
  5. 错误处理:预期内错误就地 try-catch,非预期异常冒泡到 app.onError()
  6. 测试:创建 mini-app 挂载中间件,用 app.request() 验证行为

中间件开发到这一节已经覆盖了从形式到类型的完整链路。接下来几节会逐个拆开 Hono 内置的中间件,看它们各自解决什么问题、配置项怎样影响行为。

下一篇看日志中间件——logger() 的输出格式、自定义 logger 的接入方式,以及结构化日志在 Hono 里的实现路径。