Hono — 多运行时 Web 框架
已复核Hono 是一个以 Web 标准 Request / Response 为入口的多运行时 Web 框架。日常类比:你只造一台发动机,再按底盘换接口;业务代码写的是 app.fetch(request),Cloudflare Workers、Bun、Deno 可以直接接,Node 则要另找适配器。
你写:
import { Hono } from "hono"
const app = new Hono()app.get("/", (c) => c.text("Hi"))app.get("/users/:id", (c) => c.json({ id: c.req.param("id") }))export default app固定 4.13.5 里,export default app 暴露的是 fetch;运行时只要能把 HTTP 请求变成 Request、把返回值当 Response,就能挂上同一份 app。
不理解 Hono 的入口和路由器选择,就解释不了下面几件事:
- 为什么同一份 handler 能在 Workers / Bun / Deno 上直接跑,却不能假设 Node 也在核心包里
- 为什么默认
Hono和hono/quick、hono/tiny用的不是同一套路由器 - 为什么只有一个 handler 时看不到
compose,多个中间件时next()又不能调用两次 - 为什么
HEAD请求会先按GET走一遍,再被换成空 body
Hono 的请求链可以拆成五步:
-
注册路由:
app.get(path, ...handlers)把 method、合并后的 path 和 handler 推进router.add(),同时记入routes。 -
选择路由器:默认
SmartRouter带着RegExpRouter和TrieRouter。第一次match()时按顺序把全部路由灌进候选路由器,谁能吃下就锁定,之后match直接绑到它。 -
入口是
fetch:#dispatch用getPath(request)取路径(默认strict=true),再router.match(method, path)。HEAD会改派成GET,然后用空 body 包一层Response。 -
组成中间件:匹配结果只有一个 handler 时直接调用,不再
compose;多个 handler 才走 koa 风格洋葱链。next()被调用两次会抛错。 -
Context 出响应:handler 返回
Response,或通过c.text()/c.json()写入c.res。未 finalize 时走 not-found;带getResponse()的错误可直接变成响应。
案例 1:同一份 app,换入口而不是换 handler
Section titled “案例 1:同一份 app,换入口而不是换 handler”import { Hono } from "hono"
const app = new Hono()app.get("/users/:id", (c) => c.json({ id: c.req.param("id") }))
export default appWorkers / Bun / Deno 把这份对象当 fetch 入口。固定源码里的 src/adapter 覆盖 Bun、Deno、Cloudflare、AWS Lambda、Lambda@Edge、Vercel、Netlify 和 service-worker;没有 Node adapter。要在 Node 上 listen,需要核心仓之外的包,本轮未打开那个包。
案例 2:CORS 必须先于会拒绝 OPTIONS 的中间件
Section titled “案例 2:CORS 必须先于会拒绝 OPTIONS 的中间件”import { Hono } from "hono"import { cors } from "hono/cors"import { jwt } from "hono/jwt"
const app = new Hono()app.use("*", cors())app.use("/admin/*", jwt({ secret: "xxx" }))app.get("/admin/users", (c) => c.json({ ok: true }))固定 CORS 在 OPTIONS 上直接 return new Response(null, { status: 204 }),不会 await next()。如果 jwt 先注册并匹配到预检请求,预检会在 CORS 之前被拒绝。
案例 3:路由器预设不是同一个默认值
Section titled “案例 3:路由器预设不是同一个默认值”import { Hono } from "hono"import { Hono as QuickHono } from "hono/quick"import { Hono as TinyHono } from "hono/tiny"
const app = new Hono() // SmartRouter(RegExpRouter, TrieRouter)const quick = new QuickHono() // SmartRouter(LinearRouter, TrieRouter)const tiny = new TinyHono() // PatternRouter旧文把 LinearRouter 写成默认实现,这与 4.13.5 的 src/hono.ts 不符。路由少、想先线性扫描时才走 hono/quick。
-
把 Node 当成核心 runtime:
src/adapter没有 Node。export default app对 Node http.Server 不够,还要单独 adapter。 -
把 LinearRouter 当默认:默认是 RegExpRouter 优先的 SmartRouter;LinearRouter 在
hono/quick。 -
中间件顺序:CORS 对 OPTIONS 短路。鉴权中间件放在 CORS 前面,浏览器预检过不了。
-
忘记返回 Response 或
await next():多 handler 路径在context.finalized === false时会抛 “Context is not finalized”。 -
把
c.req.param('id')的类型当成永远是string:路径里声明了必填:id时是string;未知 key 或可选参数的重载是string | undefined。
适用 vs 不适用场景
Section titled “适用 vs 不适用场景”适用:
- 目标运行时接受
(request) => Response,例如 Workers、Bun、Deno - 希望一份路由表挂到多个 adapter,而不是为每个平台重写 handler
- 需要内置 CORS / JWT / validator,并且能接受 TypeScript 路径参数推导
不适用:
- 必须跑在 Node,又不想引入核心仓之外的 server adapter
- 已经堆了大量 Express middleware,迁移成本高于重写
- 需要本轮未核验的固定 bundle 大小或路由 benchmark 数字来做选型
固定版本边界
Section titled “固定版本边界”- 本文绑定
honojs/hono@06880c4a...,annotated tagv4.13.5、package 与 npmgitHead均为同一提交。 - package 声明
engines.node >= 16.9.0,同时提供 import / require exports。 - 默认路由器是
SmartRouter + RegExpRouter,失败才落到TrieRouter;探测完成后match被替换,路由表不再允许继续add。 - 本文未安装依赖、运行
runtime-tests或测量 bundle,状态保持UNVERIFIED。
- 跨运行时靠的是 Fetch 合同,不是“全能 adapter 目录”——核心包里有哪些 adapter,要以
src/adapter为准。 - 默认路由器是启动时一次性竞选——SmartRouter 把第一次 match 当成编译点。
- 单 handler 快路径和洋葱链是两条代码——不要用 compose 的语义去解释所有请求。
- 中间件短路是可观察合同——CORS 对 OPTIONS 直接 204,顺序必须写进注册表。
new Hono()默认会先尝试哪两个路由器?LinearRouter在哪个预设?- 某条路由只匹配到一个 handler 时,还会走
compose吗? - 核心仓的
src/adapter有没有 Node?HEAD /users/1会不会原样当 HEAD 去 match?
检查点:
- 先
RegExpRouter再TrieRouter;LinearRouter在hono/quick。 - 不会。单 handler 直接调用。
- 没有 Node adapter。
HEAD会按GETdispatch,再包成空 body 响应。
- 官方文档:hono.dev
- 固定源码:honojs/hono —— 本文绑定提交
06880c4a2b04de9dd74217f26dd831209b9c01f1 - 对照入口:
src/hono.ts、src/hono-base.ts、src/compose.ts、src/router/smart-router/router.ts - trpc —— 同样 TypeScript-first,但是 RPC 而不是 REST fetch 入口
- koa —— Hono
compose注释写明基于 koa-compose
- arktype —— arktype — schema 长得像 TypeScript 类型本身
- better-auth —— better-auth — 把登录/OAuth/2FA/Passkey 拼成一行配置的 TS 认证框架
- clerk —— Clerk — 把登录注册组织 MFA 整套外包给云的 SaaS 认证 SDK
- echo —— Echo — 极简高性能 Go 框架,5 行起服务
- effect —— Effect — 给 TypeScript 装上”会跟踪错误和依赖”的副作用引擎
- elysia —— Elysia — 长在 Bun 上的极致类型安全 Web 框架
- express —— Express — Node.js 最经典的 Web 框架
- fastapi —— FastAPI — 用 Python 类型注解写 API
- fiber —— Fiber — 把 Express 写法搬到 Go 上的高性能 web 框架
- flask —— Flask — 用装饰器把 URL 接到函数上的 Python 微框架
- koa —— Koa — async/await + ctx 对象 + 洋葱模型 的极简 Node.js web 框架
- litestar —— Litestar — 类型驱动的 ASGI 框架(原 Starlite)
- nestjs —— NestJS — 把 Angular 思想搬到 Node.js 后端的企业级框架
- next-js —— Next.js — React 全栈框架
- sinatra —— Sinatra — 用 Ruby 三行代码起一个 web 服务
- spin —— Spin — 用 WebAssembly 模块当 serverless handler 的开源框架
- starlette —— Starlette — FastAPI 底下那台轻量 ASGI 引擎
- trpc —— tRPC — TS 端到端类型安全 RPC