Hono应用实例的本质

要点

  • new Hono() 创建的是一个「请求处理器容器」,不是一个 HTTP 服务器
  • app 内部维护着路由表、中间件栈、错误处理器和 404 处理器
  • app.fetch(request, env, executionCtx) 是请求处理的唯一入口
  • export default app 能直接用于 Cloudflare Workers,是因为 app 实现了 Fetch Handler 接口
  • 泛型参数 EnvRoutesBasePath 只影响类型推导,不改变运行时行为
  • 理解 app 的本质后,后续所有中间件、路由匹配机制都有了着落点

1. 一个最小例子引发的问题

先看一个最简单的 Hono 应用:

import { Hono } from 'hono'
 
const app = new Hono()
 
app.get('/', (c) => c.text('Hello Hono!'))
 
export default app

四行核心代码,跑在 Cloudflare Workers 上就能接收请求、返回响应。

这段代码写起来很直觉,但背后有几个问题值得想清楚:

  • app 到底是什么?一个普通对象?一个类实例?一个函数?
  • app.get() 调用时,路由信息存到了哪里?
  • 请求进来时,谁负责匹配路由、执行中间件、返回响应?
  • export default app 导出了一个对象,Workers 运行时拿到它之后做了什么?

这些问题不搞明白,不影响你把 CRUD 写完。但一旦遇到中间件顺序不对、错误没被捕获、类型推导报错这类情况,理解 app 的内部结构能帮你快速定位原因。

2. app 对象的内部结构

new Hono() 返回的是一个 Hono 类的实例。这个实例内部维护着几块核心数据:

Hono 实例
├── router          路由表:按 HTTP 方法组织的树形结构,存储路径 → handler 的映射
├── middleware      中间件栈:通过 app.use() 注册的中间件队列
├── errorHandler    错误处理器:通过 app.onError() 设置的兜底函数
├── notFoundHandler 404 处理器:通过 app.notFound() 设置的未匹配处理函数
└── fetch           请求入口:app.fetch() 方法是外部调用的唯一入口

当你调用 app.get('/users', handler) 时,Hono 做的事情是把路径 /users、方法 GET 和对应的处理函数 handler 存入路由表。当你调用 app.use('/api/*', middleware) 时,Hono 把中间件追加到中间件栈里。

这些注册操作只是「收集配置」,不涉及任何请求处理。请求处理是另一个阶段的事。

3. fetch handler 模式

理解 Hono 的关键在于一个概念:app 本质上是一个实现了 fetch 方法的对象。

Web Standards 定义了一种叫 Fetch Handler 的接口——任何拥有 fetch(request: Request): Response 方法的对象,都可以处理 HTTP 请求。Cloudflare Workers、Deno Deploy、Bun 等运行时都认这个接口。

Hono 的 app 对象恰好就是这个形状:

// Hono app 的 fetch 签名(简化版)
app.fetch(
  request: Request,    // 标准 Web Request 对象
  env?: E,             // 运行环境绑定(如 Workers 的 env)
  executionCtx?: ExecutionContext  // 执行上下文(如 waitUntil)
): Response | Promise<Response>

三个参数分别对应不同运行时的特性:

  • request — 所有运行时都有的标准请求对象
  • env — Cloudflare Workers 特有的环境变量和绑定资源
  • executionCtx — Workers 的 ExecutionContext,用于 waitUntil() 等异步延续

在 Node.js 环境中运行时,后两个参数通常为空,只有 request 是必需的。

4. app.fetch() 的工作流程

app.fetch() 是整个请求处理的入口。当一个请求到来时,内部大致经历这几步:

请求到达
  ↓
① 创建 Context 对象(包装 request,提供 c.req / c.json 等 API)
  ↓
② 根据 method + path 查路由表,找到匹配的处理函数
  ↓
③ 收集路径上所有匹配的中间件
  ↓
④ 按注册顺序组装执行链:中间件A → 中间件B → 路由handler
  ↓
⑤ 依次执行,每个中间件可通过 await next() 把控制权交给下一层
  ↓
⑥ 路由 handler 返回 Response,沿调用栈回传
  ↓
⑦ 如果中途抛异常 → 走 errorHandler
   如果没匹配到路由 → 走 notFoundHandler
  ↓
返回 Response

有几个关键点值得注意:

第一,Hono 不会修改原始的 Request 对象。所有框架级别的状态——路由参数、中间件写入的变量、请求上下文——都挂在 Context 对象上。Request 保持干净,通过 c.req.raw 随时可以拿到原始对象。

第二,路由匹配和中间件收集是在同一次遍历中完成的。Hono 的路由器(默认使用基于三元搜索树的高效实现)会在匹配具体路由的同时,把路径上所有 app.use() 注册的通配中间件一并收集。

第三,错误传播是可控的。任何一个中间件或 handler 里抛出的异常,都会被 Hono 内部捕获并交给 app.onError() 处理。如果你没有自定义错误处理器,Hono 会返回一个 500 响应。

5. export default app 的机制

在 Cloudflare Workers 的模块格式中,入口文件需要导出一个包含 fetch 方法的对象作为默认导出:

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    // 处理请求并返回 Response
  }
}

Hono 的 app 对象恰好满足这个形状。所以 export default app 直接把 app 交给 Workers 运行时,运行时在收到请求时调用 app.fetch(request, env, ctx)

整个过程没有任何魔法——只是 JavaScript 的对象导出 + 方法调用。

这也是 Hono 能同时跑在 Workers、Deno、Bun、Node.js 上的原因。这些运行时都支持 Fetch Handler 模式,而 Hono 的 app 就是这个模式的一个实现。不需要针对不同平台写适配层,只需要一个 app.fetch() 就够了。

6. Hono 类的构造参数

你可能注意到 new Hono() 支持泛型参数:

const app = new Hono<{ Bindings: Env }>()

这些泛型参数——EnvRoutesBasePath——只影响 TypeScript 的类型推导,不改变任何运行时行为。

Env — 环境变量类型

声明运行时的绑定资源类型,让 c.env 有正确的类型提示:

type Bindings = {
  DB: D1Database
  KV: KVNamespace
  API_KEY: string
}
 
const app = new Hono<{ Bindings: Bindings }>()
 
app.get('/data', (c) => {
  const db = c.env.DB     // 类型:D1Database
  const key = c.env.API_KEY // 类型:string
  return c.json({ ok: true })
})

Routes — 路由类型

通常用于 app.route() 组合子应用时,让主应用继承子应用的路由类型定义。单个应用很少需要手动指定。

BasePath — 基础路径类型

创建带路径前缀的子应用时使用:

const api = new Hono().basePath('/api/v1')
 
api.get('/users', (c) => c.text('users'))
// 实际路径:GET /api/v1/users

三个泛型参数都有默认值,所以 new Hono() 不传任何参数完全合法。类型系统会随着你添加路由和中间件逐步推导,大部分场景下不需要手动标注。

7. 与其他框架的入口对比

把 Hono 和 Express 放在一起看,能更清楚地理解两者的设计差异。

Express:应用自身启动服务器

import express from 'express'
 
const app = express()
 
app.get('/', (req, res) => {
  res.send('Hello')
})
 
// app 自己启动 HTTP 服务器,监听端口
app.listen(3000, () => {
  console.log('Server running on port 3000')
})

Express 的 app 既是路由容器,也是服务器管理者。app.listen() 内部调用 Node.js 的 http.createServer(),把请求处理逻辑绑定到 TCP 端口。

这意味着 Express 和 Node.js 的 HTTP 模块是绑定的。想在其他运行时跑 Express,需要额外的适配层(比如用 @now/node 或者 serverless adapter)。

Hono:应用只是请求处理器

import { Hono } from 'hono'
 
const app = new Hono()
 
app.get('/', (c) => c.text('Hello'))
 
// 不启动服务器,不监听端口
// 直接导出,让运行时来调用 app.fetch()
export default app

Hono 的 app 只是一个请求处理器。它不关心请求从哪里来——可以是 Workers 的 fetch event,可以是 Node.js 的 HTTP 服务器,也可以是另一个函数的调用。

在 Node.js 中使用 Hono 时,需要用 @hono/node-server 把它包一层:

import { serve } from '@hono/node-server'
import { Hono } from 'hono'
 
const app = new Hono()
 
app.get('/', (c) => c.text('Hello'))
 
// 用适配器把 app.fetch 接入 Node.js 的 HTTP 服务器
serve({ fetch: app.fetch, port: 3000 })

这个区别不是「谁更好」的问题,而是设计取向的不同。Express 诞生在 Node.js 独占服务端 JavaScript 的时代,和 http 模块绑定是自然的选择。Hono 诞生在多运行时时代,Web Standards 的 Fetch API 已经成为跨平台公约数,所以选择了更解耦的方式。

延伸阅读

总结

new Hono() 创建的 app 对象是一个请求处理器容器。它内部维护路由表和中间件栈,对外暴露 fetch() 方法作为请求入口。export default app 之所以能直接用于 Cloudflare Workers,是因为 app 实现了标准的 Fetch Handler 接口。

泛型参数 EnvRoutesBasePath 服务于类型安全,不影响运行时逻辑。与 Express 的 app.listen() 不同,Hono 的 app 不管理服务器,只处理请求——这让同一个 app 实例能跑在 Node.js、Workers、Deno、Bun 等各种运行时上。

理解了 app 的本质,后续关于中间件洋葱模型、路由匹配策略、错误传播机制的讨论就有了明确的着落点。下一篇展开讲路由注册与匹配的底层机制。