当前位置:首页 > 技术干货 > 正文内容

藏在 Nuxt 底下的 H3 发布了 v2:一个极简 HTTP 框架,凭什么被整个生态依赖

小悟3个月前 (07-25)技术干货10

一、先做个实验:在你的项目里跑一行命令

1
2
3
npm ls h3
# 或者
pnpm why h3

如果你维护过任何一个 Nuxt 项目,大概率会看到一个你从没主动安装过的包出现在依赖树里。它的名字叫 h3,念作 /eɪtʃθriː/,全名是 “minimal h(ttp) framework”。

我本地没有 Nuxt 项目,所以换个方式验证:直接去 npm 官方的下载量接口查了一下它上一周(2026-10-21 至 10-27)的安装量——32,450,948 次。

(顺带说一句:网上流传的”H3 被 10 万+ 项目依赖”这个说法,我没找到原始出处,大概率是 GitHub 侧栏 “Used by” 计数被转述出来的。方向没错,但这个具体数字我不敢用,也不建议你引。上面的下载量是我自己查的一手数据,接口地址见文末参考资料。)

一个周下载三千多万次的包,为什么大多数人从没听说过?因为它几乎从不出现在 package.json 的第一行。你装的是 Nuxt,nuxt build 产出的 .output/server/index.mjs 底下跑的就是它。写了多年 Nuxt 的人,很可能一次都没写过 import { H3 } from "h3"。

2026 年 10 月 3 日,H3 v2 宣布稳定。真正把它推进公众视野的,是同期 JavaScript Weekly 的一条头条推荐。

二、H3 到底站在哪一层

要理解这次更新,先得搞清楚它在技术栈里的位置。我按官方文档整理了一张分层图:

你的业务代码handler / middlewareh3 核心路由 · 中间件 · 内置工具只认 Web 标准Request / Responsesrvx 通用 server 层按运行时条件导出不同入口NodeBunDenoWorkers
图:H3 在服务端的分层位置。整理自 h3.dev/blog/v2 与 srvx 文档。

关键点在于:H3 自己不起服务器。真正监听端口的是它下面那层叫 srvx 的东西。H3 只做三件事——把请求交给你写的 handler、把你返回的值转成 Response、以及中间的路由和中间件。

这个切分是它当年能从 Node 一路跑到 Cloudflare Workers 的根本原因。也正因为这样,”H3 性能好不好”这个问题其实问错了对象,性能的大头在 srvx 和你选的运行时上。

三、v2 到底重写了什么

官方博客给的一句话定义是:基于 Web 标准的完整重写,兼容大部分 v1 工具函数,比 v1 更快、更小。我按官方说法逐条拆开,尽量不添油加醋。

1. 只认 Request 和 Response

v1 的 H3Event 是自己封装的一套请求抽象。v2 把它换成了浏览器里那四个原生对象:Request、Response、URL、Headers。

1
2
3
4
5
6
7
8
9
10
import { defineHandler } from "h3";

const handler = defineHandler(async (event) => {
 const { pathname, searchParams } = event.url;
 const accept = event.req.headers.get("Accept");
 const body = await event.req.json();

 event.res.headers.set("x-powered-by", "h3");
 return { hello: "web" };
});

返回值可以是字符串、对象、Response、流、Blob——H3 统一帮你转成 Response。这意味着你在 Express 里积累的那些 res.status().json() 肌肉记忆,在这里全部作废,取而代之的是你在 fetch 里已经写熟的东西。

2. 同一份代码,跑在五个地方

官方列出的运行时是:Node.js、Bun、Deno、Cloudflare Workers、Service Workers 以及浏览器。

实现方式是条件导出,不是运行时探测。我把装好的包翻出来看了一眼 dist/_entries/ 目录,实际存在这些入口文件:

1
bun.mjs  deno.mjs  cloudflare.mjs  node.mjs  service-worker.mjs  generic.mjs

同一个 import "h3",在 Node 里解析到 node.mjs,在 Bun 里解析到 bun.mjs。这个设计有个副作用值得记一下:你看到的报错栈里,解析出来的文件路径是排查的第一线索。同一个 bug 在 Node 上复现、在 Bun 上不复现,很可能只是因为它们走的根本不是同一个文件。

(我机器上没装 Bun 和 Deno,所以跨运行时这块我只验证了产物文件确实存在,没有实跑。不想假装测过。)

3. 路由交给 rou3

v2 用 rou3 v1 做路由匹配,语法接近 URLPattern:

1
2
3
app.get("/users/:id(\\d+)", (event) => event.context.params.id);
app.get("/blog/:id(\\d+){-:title}?", (event) => event.context.params);
app.get("/files/:path+", (event) => event.context.params.path);

rou3 仍然是按段在树上匹配,所以查找不慢。官方也明确说了它和 URLPattern 有几处故意的不同,比如忽略末尾多余的一个斜杠。

4. 中间件、插件、类型

中间件是 (event, next) 的洋葱模型,写法和 Koa 系接近:

1
2
3
4
5
app.use(async (event, next) => {
 const body = await next();
 event.res.headers.append("x-middleware", "works");
 return body;
});

类型这块,handler、响应和错误(HTTPError)都是带类型的;校验走 Standard Schema,也就是 zod、valibot 这类库都能接。

5. route rules:把 Nginx 的活搬进框架

这是 v2 新加的 h3/rules 引擎,我第一眼看到时觉得挺意外:

1
2
3
4
5
6
7
8
9
import { routeRules } from "h3/rules";

app.use(
 routeRules({
   "/old/**": { redirect: "/new/**" },
   "/assets/**": { headers: { "cache-control": "s-maxage=31536000" } },
   "/api/**": { cors: true },
 }),
);

重定向、缓存头、CORS、代理,以前这些要么写在 Nginx 配置里,要么自己糊中间件,现在是一个配置对象。往好了说是内聚,往坏了说是框架的边界在往外扩——这件事到底算优点还是负担,得看你的部署形态。如果你的服务前面还挂着一层 Nginx,这套规则就等于把配置分散到了两个地方。

6. 内置工具

官方列出的有:Cookie(含分块 Cookie)、session、CORS、代理、静态文件、缓存头、SSE、WebSocket(通过 crossws)、JSON-RPC / MCP 辅助函数,以及一个 h3/tracing 插件。

顺带说一句,这里出现 MCP 辅助函数不是偶然。当框架开始内置 MCP 的时候,说明”给 AI 当后端”已经从实验变成了默认场景。我在《什么是 Agent:从”会聊天”到”会干活”》里写过 Agent 需要工具才能碰到真实世界,而 MCP 就是那个工具协议的标准答案之一。

四、实测:1.0 MB vs 3.8 MB

官方说”比 v1 更快更小”,但没给数字。我干脆自己测了一遍。

环境:MacBook,Node.js v22.22.2,h3 2.0.8,Express 5.3.0。两个框架分别单独安装到干净目录,跑一个只有两个路由(一个返回纯文本、一个返回 JSON)的最小服务。

安装体积与依赖数量


h3 2.0.8Express 5.3.0
node_modules 体积1.0 MB3.8 MB
顶层依赖目录数3(h3、rou3、srvx)65

我去 registry 核了一下 h3 的运行时依赖字段,确认只有两个:rou3: ^1.0.0 和 srvx: ^1.0.6。装完只多出三个目录,跟字段是对得上的。

65 个包对 3 个包,这个差距不是”体积”层面的事,是供应链攻击面的事。

冷启动与吞吐

启动时间取的是”进程 spawn 到第一个请求返回 200”的间隔;吞吐是 50 并发、keep-alive、持续 3 秒,每项跑 3 次取中位数。


冷启动纯文本 rpsJSON rps
h3 2.0.8487 ms10,5679,655
Express 5.3.01,392 ms4,8765,233

几个必须说清楚的地方,免得这组数字被误读:

  • 第一次跑 h3 时数据明显偏低(冷启动 642 ms、纯文本只有 5,265 rps),后面两次才稳定。偏低的原因是文件缓存没热,我取的是三次中位数。

  • 我的压测脚本就是个几十行的 node:http 循环,不是 wrk / autocannon 那种专业工具。同一台机器、同一个 Node 版本、同一个进程模型下跑出来的相对差距有参考意义,绝对值别拿去当生产指标。

  • 两者都在 Node 的 http 模块上跑,所以差距主要来自框架自身的开销,而不是底层 I/O。

冷启动差了近 3 倍这件事,在 Serverless 和边缘场景下是实打实的钱。冷启动时间直接计入计费,也直接计入用户等第一个字节的时间。

五、最被低估的一点:handler 可以直接调用

我测下来觉得最实用的是这个——

1
2
3
4
const app = new H3();
app.get("/hello", () => ({ hello: "web" }));

const res = await app.fetch(new Request("http://localhost/hello"));

不需要 serve(),不需要占用端口,不需要等进程起来。这是我实际跑出来的输出:

1
2
3
4
handler.fetch /hello -> 200 {"hello":"web"}
regex param -> 200 42
cors rule -> 200 origin: *
bad param -> 404

四行分别是:直接调用返回 200 和 JSON;/users/:id(\d+) 传 42 正常返回;route rules 的 CORS 生效,响应头里确实有 access-control-allow-origin: *;传 abc 这种非数字参数,路由不匹配返回 404。

这个改动的连锁反应比看起来大:单测不用起服务、不用等端口释放、不用处理并发测试时的端口冲突。以前测一个 Express 接口,要么挂 supertest,要么自己管端口生命周期;现在它就是一个纯函数,输入 Request,输出 Response。

六、为什么是现在

H3 v2 本身值得写一篇,但它出圈的时机更有意思。

同一期 JavaScript Weekly(第 806 期)里,还有一条:Hono 5.0 RC 宣布 ESM-only,并把运行时适配器拆成独立包。Hono 早就是 Web 标准路线的老玩家了。

两条消息放一起看,指向同一件事:JS 服务端框架正在集体向 Request / Response 收敛。Express 那套 req / res 的自定义抽象,正在被浏览器的原生对象替换掉。

背后的驱动力其实挺朴素——Serverless 和边缘运行时不允许你带一堆 Node 专属的东西进去,而 Web 标准是唯一各方都认的公约数。当一个框架把抽象层削到只剩标准对象时,它自己就变小了,也就更容易被塞进任何地方。

这对写业务的人有直接影响:你在浏览器里学的 fetch、Request、Headers,现在在后端可以直接用,同一套知识不用学两遍。

七、一笔安全账:CVE-2026-23527

写这篇的时候顺手查到一条,我觉得比 v2 的新特性更值得你现在就去处理。

2026 年 10 月披露的 CVE-2026-23527,影响 h3 v1 线 ≤ 1.15.4,CVSS 8.9(高危):readRawBody 在检查 Transfer-Encoding: chunked 时做了大小写敏感的比较,而这个头按规范是大小写不敏感的。攻击者发一个大小写不同的变体,服务端就会认为这个请求没有 body,于是把剩下的字节当成下一个请求的开头——也就是 HTTP 请求走私(CWE-444)。

修复版本是 1.15.5。

需要强调的是触发条件:advisory 里提到的高危场景是应用跑在四层负载均衡或不做头部规范化的代理后面。这时候代理和 h3 对”请求在哪里结束”的理解不一致,才有可能被利用。

排查就三步:

1
2
npm ls h3                 # 看有没有老版本,monorepo 里可能有多个副本
pnpm why h3

如果你装了 Nuxt 或 Nitro,即使你从没写过 import "h3",它也在你的依赖树里。升到 1.15.5 以上,或者升到 v2;跑完再 npm ls h3 确认没有遗留的旧副本。另外检查一下你的代理有没有规范化 Transfer-Encoding 和 Content-Length。

八、什么时候该用 H3,什么时候不该

极简是有代价的,代价就是你得自己决定很多事。我自己的判断标准是这样:

该用:

  • 要在 Electron 之类的桌面应用里起一个本地 HTTP 服务(比如局域网配对页、控制接口、设备发现)。包小、启动快、一条 serve(app, { port }) 就能跑。

  • 同一份 handler 要部署到不同运行时(今天是 Node,明天可能迁 Workers)。

  • 写库或者中间件给别人用——你的东西不该替用户决定用哪个框架。

  • 需要 SSE 推送(投屏画面帧、进度流、日志流这类场景),H3 内置了。

不该用:

  • 你想要一个框架替你决定目录结构、ORM、校验库。H3 明确说自己 “minimally opinionated”,它的 examples 目录就是一堆单文件 .mjs,没有任何脚手架。

  • 你的团队熟悉 Express 生态,且项目跑在传统服务器上。迁移的收益覆盖不了重写成本。

  • 你需要大量现成中间件。Express 十几年的积累不是 H3 该比的维度。

如果你想要的是”H3 的能力加上一套架构约束”,那该看的是构建在它之上的 Nitro。Nuxt 的做法就是这么分层:把 H3 当零件用。

顺带一提,这也是我一直觉得技术选型里最值钱的那件事——《前端已死三个月后:活下来的人,靠的不是 React》里我管它叫”判断力密度”。难的是知道一个包该待在哪一层、以及它替你承担了什么。

结语

H3 是个挺特别的案例:装机量大到离谱,名字却几乎没人提。它的价值恰恰在于它不试图成为你技术栈里的主角——它只想做那一层薄得几乎看不见的适配,让你写的 handler 能在任何地方跑起来。

所以”凭什么被整个生态依赖”这个问题的答案,可能有点反直觉:正因为它足够不显眼。

一个基础设施的成熟度,往往和它存在感的强度成反比。你感觉不到它的时候,说明它做对了。


相关文章

AI智能体(Agent)全解析:从零基础到精通的终极指南

一、AI智能体的定义与核心内涵AI智能体(Agent)是能够感知环境、自主决策并执行动作的智能实体,其核心特征在于“自主性”“反应性”“目标导向性”与“社会交互性”。与传统程序不同,AI智能体通过传感...