H3 v1 到 v2 迁移:核心变更
H3 v2(稳定版 2.0.8,2026-10-22 发布)是一次基于 Web 标准的完整重写:更快、更小,并且保留了绝大多数 v1 工具的兼容写法。它不再是「加了新功能的 v1」,而是底层换成了浏览器和运行时原生就有的 Request / Response / Headers / URL。
💡 一个会让你困惑的点:官方迁移页至今仍写着 "H3 v2 in beta stage"。这是文档措辞滞后——v2 早已正式发布(详见 8.3)。本教程一律按「v2 已稳定」表述,文中凡出现旧说法都以正式版为准。
这一节讲四个最影响迁移的变化:Node 版本与纯 ESM、Web 标准重写、响应处理模型、H3 与 Router 合并。8.2 才是逐项的弃用对照表。
一、Node.js 版本与纯 ESM
H3 v2 要求运行在 Node.js >= 20.19(推荐最新 LTS)。
它现在是纯 ESM 包——没有 require 入口。如果你的存量项目还在用 CommonJS(require 和 module.exports),最新 Node.js 的 require(esm) 能力让你暂时还能直接 const { H3 } = require("h3") 用下去,给迁移留出缓冲;但这不是长期方案,最终应改成 ESM 的 import。
// CommonJS 项目:靠 Node 的 require(esm) 能力,暂时还能跑
const { H3, serve } = require("h3");
// 推荐改法:整包切到 ESM
import { H3, serve } from "h3";
其他运行时(Bun、Deno)原生支持 ESM,不受影响。这也是 v2「随处运行」的基础(详见 8.3)。
二、Web 标准重写
v2 不再维护自己的一套「请求 / 响应对象」,而是直接复用 Web 平台的四个基本类型:
event.req:标准Request(请求)event.res:标准Response(响应)event.url:标准URLevent.res.headers/event.req.headers:标准Headers
Node.js 运行时下,H3 通过一个兼容层(srvx)把 Node 的 http.IncomingMessage 桥接成 Web 标准接口;其他运行时则用原生 Web 兼容 API。
最显眼的改名:event.web 改名为 event.req。如果你在 v1 里通过 event.web 拿 Web Request,迁移时全部替换成 event.req 即可。
// v1:通过 event.web 拿标准 Request
const body = await event.web.json();
// v2:event.web 改名 event.req
const body = await event.req.json();
一个边界要注意:只有在 Node.js 运行时,才能访问原生的 event.node(即 Node 的 req / res)。event.node 在 v2 被挪到了 event.runtime.node——详见 8.2 的「废弃属性」一节。
三、响应处理模型(最容易漏的迁移点)
v1 靠 send* 系列函数把响应「写」出去;v2 改成始终显式 return 响应体,或 throw 错误。H3 会智能识别你返回的值(文本、JSON、流、Web Response 等)并自动转成 Response。
下面是 10 个最常用的迁移对照——*光看表格不够,要确认每个 handler 末尾真的把 `send换成了return`**:
| v1 写法 | v2 写法 | 说明 |
|---|---|---|
send(event, value) |
return value |
返回文本 / JSON / 任意可序列化值 |
sendError(event, err) |
throw createError(err) |
错误改为抛出,由 H3 统一渲染 |
sendStream(event, stream) |
return stream |
返回流或 Response |
sendWebResponse(event, res) |
return res |
直接返回 Web Response |
sendNoContent(event) / return null |
return noContent() |
204 空响应 |
sendIterable(event, value) |
return iterable(value) |
迭代器转流 |
sendProxy(event, target) |
return proxy(event, target) |
反向代理 |
sendRedirect(event, loc, code) |
return redirect(loc, code) |
重定向 |
serveStatic(event, content) |
return serveStatic(event, content) |
必须补 return,否则响应永远发不出去 |
handleCors(event) |
先判断返回值,非 false 就提前 return |
返回值被忽略是 v1 迁移最常见的 bug |
handleCors 这一行要单独强调:v1 里 handleCors(event) 就地改 header,调完继续往下走;v2 里它返回一个 Response(命中预检时)或 false(放行)——你如果还像以前一样「调完就不管」,预检请求会拿着空响应被放行到后续 handler,CORS 等于没生效。正确写法是 const res = handleCors(event); if (res !== false) return res;。
// v1:send 就地写出
app.use("/", (event) => {
send(event, { hello: "h3" });
});
// v2:直接 return,H3 自动转 Response
app.use("/", (event) => {
return { hello: "h3" };
});
为什么 return 比 send 更好?send* 是「命令式地写到一个可变响应对象上」,handler 拿不到自己写出去的东西;return 是「把一个值交给框架」,这个值可以被中间件拦截、改写、包裹,天然支持 async (event, next) => { const body = await next(); return body; } 这种「包裹式中间件」。这也是 v2 能统一中间件模型的原因(见 8.3)。
四、H3 与 Router 合并
v1 里 app 和 router 是两个独立对象(createApp + createRouter),v2 把它们合并进同一个 H3 实例:原来 router 上的方法(get / post / use / add …)现在全部挂在 app 上。
// v1:app 与 router 分开
const app = createApp();
const router = createRouter()
.get("/", () => "hi");
app.use(router);
// v2:一个 new H3() 全包了
const app = new H3().get("/", () => "hi");
配套的路由匹配引擎换成了 rou3,行为更直观,但有几处破坏性变化必须逐一核对:
- 中间件不再匹配子路径:
app.use("/path", handler)现在只匹配/path这一个路径。要像以前那样匹配/path/foo/bar等所有子路径,必须写成app.use("/path/**", handler)。这是 v1→v2 迁移里最高频的「为什么我的中间件不生效了」的原因。 event.path带上完整前缀:每个 handler 收到的event.path不再省略app.use时声明的路径前缀(v1 会剥掉前缀)。前缀场景统一用withBase(base, handler)生成带前缀的子应用,例如withBase("/api", app.handler)。router.add参数顺序反转:router.add(path, method)变成router.add(method, path)——方法在前、路径在后,且method现在接受单个方法或方法数组。router.use(path, handler)已废弃:改用router.all(path, handler)。app.resolve(path)已移除:不再提供显式路径解析方法。- lazy 写法变化:
app.use(() => handler, { lazy: true })不再支持,改成app.use(defineLazyEventHandler(() => handler), { lazy: true })。 - 数组参数不再支持:
app.use(["/p1", "/p2"], ...)和app.use("/path", [h1, h2])都失效,改为多次app.use()调用。
// v1:router.add(path, method)
router.add("/users/:id", "GET", handler);
// v2:router.add(method, path),method 可为数组
app.add("GET", "/users/:id", handler);
app.add(["GET", "POST"], "/users", handler);
迁移自查清单
把这一节和 8.2 合起来,逐条过一遍你的代码:
require("h3")还能跑,但计划切到import(Node>= 20.19)。event.web→event.req;需要 Node 原生对象时找event.runtime.node。- 所有
send*改成return;sendError改成throw createError;serveStatic和handleCors别忘了补return。 createApp/createRouter→new H3();router.add参数顺序反过来;router.use→router.all。- 所有
app.use("/path", ...)想匹配子路径的,补成/path/**。 event.path现在带完整前缀,前缀逻辑改用withBase。
下一节(8.2)是完整的弃用对照表,按「请求体 / Cookie 与 Headers / 工具函数 / Node 与 Web 适配 / 类型导出」五组列出 v1→v2 的逐一替换。