H3 v1 到 v2 迁移:核心变更

2026-10-30 17:48 更新

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:标准 URL
  • event.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 合起来,逐条过一遍你的代码:

  1. require("h3") 还能跑,但计划切到 import(Node >= 20.19)。
  2. event.web → event.req;需要 Node 原生对象时找 event.runtime.node。
  3. 所有 send* 改成 return;sendError 改成 throw createError;serveStatic 和 handleCors 别忘了补 return。
  4. createApp / createRouter → new H3();router.add 参数顺序反过来;router.use → router.all。
  5. 所有 app.use("/path", ...) 想匹配子路径的,补成 /path/**。
  6. event.path 现在带完整前缀,前缀逻辑改用 withBase。

下一节(8.2)是完整的弃用对照表,按「请求体 / Cookie 与 Headers / 工具函数 / Node 与 Web 适配 / 类型导出」五组列出 v1→v2 的逐一替换。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号