Skip to content

中间件的本质:(c, next) => Promise<void> 的函数式组合

next() 调用前 = 请求阶段,next() 后 = 响应阶段

在 Hono 中,中间件(Middleware)并非一个复杂的抽象,而是回归其本质:一个接受上下文 cnext 函数的异步函数。其类型签名清晰地定义了其行为:

ts
(c: Context, next: Next) => Promise<void>

这一简洁的设计,使得中间件成为一种典型的函数式编程模式,支持组合、复用和分层,构成了 Hono 扩展能力的核心。


一、中间件的执行模型:洋葱模型(Onion Model)

Hono 的中间件采用经典的“洋葱模型”执行。当多个中间件被注册时,它们的执行流程如下:

        外层中间件


     [请求阶段] ←─┐
          │       │
          ▼       │
     内层中间件   │
          │       │
          ▼       │
     [请求阶段]   │
          │       │
          ▼       │
       处理器      │
          │       │
          ▼       │
     [响应阶段] ──┘


     内层中间件


     [响应阶段]


        外层中间件

关键在于 next() 函数的调用时机:

  • next() 调用前:属于“请求阶段”(Request Phase),你可以修改请求、读取头、验证权限等;
  • next() 调用后:属于“响应阶段”(Response Phase),你可以修改响应、记录日志、添加头等。

二、next() 的作用:控制执行流

next() 是一个函数,调用它表示“将控制权交给下一个中间件或最终处理器”。它的返回值是一个 Promise<void>,但更重要的是,你可以 await next(),从而在后续中间件和处理器执行完毕后,继续执行当前中间件的剩余逻辑。

示例:日志中间件
ts
const logger = async (c: Context, next: Next) => {
  const start = Date.now()

  // 请求阶段:记录请求开始
  console.log(`Request: ${c.req.method} ${c.req.url}`)

  // 调用 next(),等待内层逻辑执行完毕
  await next()

  // 响应阶段:记录响应完成
  const duration = Date.now() - start
  console.log(`Response: ${c.res.status} in ${duration}ms`)
}

在这个例子中:

  • await next() 之前,是请求阶段;
  • await next() 之后,是响应阶段;
  • c.resnext() 调用后才存在,因为响应是由内层处理器生成的。

三、中间件的函数式特性

Hono 的中间件设计体现了函数式编程的核心思想:

  1. 高阶函数
    中间件本身是一个函数,它接受 cnext 并返回 Promise<void>。你也可以创建返回中间件的函数,实现参数化:

    ts
    const auth = (role: string) => {
      return async (c: Context, next: Next) => {
        const userRole = c.req.header('X-User-Role')
        if (userRole !== role) {
          return c.json({ error: 'Forbidden' }, 403)
        }
        await next()
      }
    }
    
    app.use('/admin/*', auth('admin')) // 生成一个只允许 admin 的中间件
  2. 组合性(Composition)
    多个中间件可以像函数一样组合,形成处理管道:

    ts
    app.use('/api/*', logger, auth('user'), cors())

    执行顺序为:

    • logger → 请求阶段
    • auth → 请求阶段
    • cors → 请求阶段
    • 处理器
    • cors → 响应阶段
    • auth → 响应阶段
    • logger → 响应阶段
  3. 无副作用或可控副作用
    理想的中间件应尽量避免全局状态,而是通过 c.set()c.var 在上下文中传递数据:

    ts
    const userAgent = async (c: Context, next: Next) => {
      const ua = c.req.header('User-Agent') || 'Unknown'
      c.set('userAgent', ua) // 将数据注入上下文
      await next()
    }
    
    app.use('/track', userAgent)
    app.get('/track', (c) => {
      const ua = c.var.userAgent // 安全获取
      return c.json({ ua })
    })

四、中断执行流:不调用 next()

中间件可以通过不调用 next() 来中断请求流程,直接返回响应。这是实现认证、限流、CORS 预检等逻辑的关键。

示例:CORS 预检处理
ts
const cors = async (c: Context, next: Next) => {
  if (c.req.method === 'OPTIONS') {
    // 预检请求,直接返回成功,不调用 next()
    c.res = new Response(null, {
      status: 204,
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
      },
    })
    return
  }
  // 非预检请求,继续
  await next()
}

在此例中,OPTIONS 请求不会进入后续中间件或处理器,执行流在此中断。


五、错误处理中间件

Hono 支持错误处理中间件,其签名略有不同:

ts
(c: Context, error: Error) => Response | Promise<Response>

它在任意中间件或处理器抛出异常时被调用,用于统一返回错误响应:

ts
app.onError((err, c) => {
  console.error(err)
  return c.json({ error: 'Internal Server Error' }, 500)
})

六、结论:中间件是 Hono 的扩展基石

Hono 的中间件设计极简而强大:

  • 类型 (c, next) => Promise<void> 精确描述了其行为;
  • next() 的调用时机划分了请求与响应阶段;
  • 函数式组合使其易于复用和测试;
  • 不调用 next() 可中断流程,实现拦截逻辑。

正是这种设计,使得 Hono 能够通过极小的核心,支持认证、日志、压缩、验证、CORS 等丰富功能,真正实现“微框架,大生态”。