Hono应用实例的本质
要点
new Hono()创建的是一个「请求处理器容器」,不是一个 HTTP 服务器- app 内部维护着路由表、中间件栈、错误处理器和 404 处理器
app.fetch(request, env, executionCtx)是请求处理的唯一入口export default app能直接用于 Cloudflare Workers,是因为 app 实现了 Fetch Handler 接口- 泛型参数
Env、Routes、BasePath只影响类型推导,不改变运行时行为 - 理解 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 }>()这些泛型参数——Env、Routes、BasePath——只影响 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 appHono 的 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 已经成为跨平台公约数,所以选择了更解耦的方式。
延伸阅读
- Web Standards Fetch API —
Request、Response等核心概念的标准定义 - Cloudflare Workers Module Format — 理解为什么
export default { fetch }能工作 - Hono 源码 src/hono.ts —
Hono类的完整实现,核心不到 200 行
总结
new Hono() 创建的 app 对象是一个请求处理器容器。它内部维护路由表和中间件栈,对外暴露 fetch() 方法作为请求入口。export default app 之所以能直接用于 Cloudflare Workers,是因为 app 实现了标准的 Fetch Handler 接口。
泛型参数 Env、Routes、BasePath 服务于类型安全,不影响运行时逻辑。与 Express 的 app.listen() 不同,Hono 的 app 不管理服务器,只处理请求——这让同一个 app 实例能跑在 Node.js、Workers、Deno、Bun 等各种运行时上。
理解了 app 的本质,后续关于中间件洋葱模型、路由匹配策略、错误传播机制的讨论就有了明确的着落点。下一篇展开讲路由注册与匹配的底层机制。