H3 嵌套应用
一个后端长到一定程度,总会出现「这块归另一个团队」「这段已经用别的框架写好了」的情况。把所有东西塞进一个 H3 实例既难维护,也不现实。
H3 的答案很直接:mount() 把另一个应用挂到某个路径前缀下,路由和中间件自动合并。挂进来的可以是一个 H3 实例,也可以是任何 Request => Response 的 Web 应用。
挂载一个 H3 子应用
import { H3, serve } from "h3";
// 子应用:自带一条中间件和一条通配路由
const nestedApp = new H3()
.use((event) => {
event.res.headers.set("x-api", "1"); // 只对 /api 下的请求生效
})
.get("/**:slug", (event) => ({
pathname: event.url.pathname, // 完整路径,不剥前缀
slug: event.context.params?.slug, // 通配捕获到的是剩余部分
}));
// 主应用把子应用挂到 /api
const app = new H3().mount("/api", nestedApp);
请求 /api/test 时,返回值得注意两处:
| 字段 | 值 | 说明 |
|---|---|---|
pathname |
/api/test |
event.url.pathname 始终是真实完整路径,不会因为挂载而剥掉前缀 |
slug |
/test |
通配段捕获到的是去掉前缀后的剩余部分 |
这个区别很重要:路径是全局的,参数是相对的。所以子应用里写重定向、拼内部链接时可以直接用 pathname,而做路由分发时用 params。
📌 v1 里用
useBase()给应用加前缀,v2 改成了withBase();同时event.path已弃用,要拿完整路径用event.url.pathname + event.url.search。这也是 v2「路径永远不省略前缀」这条设计的一部分。
全局配置与钩子不会被继承
子应用的 H3Config 与全局钩子(onRequest、onResponse、onError)不会随挂载带入主应用:
// 子应用的配置只在子应用自己 serve 时才生效
const nestedApp = new H3({
onError: (error) => console.error("子应用的钩子(挂载后不会被调用)", error),
});
const app = new H3().mount("/api", nestedApp);
所以官方建议:钩子和全局配置一律设在主应用上。如果确实需要子应用自己的错误处理,用 onError 中间件而不是全局钩子(见 2.6)。
挂载任意 Web 标准应用
两者差异先用一张图固定下来:

只要对方符合 Request => Response,就能挂进来——Hono、Elysia 这类框架都符合:
import { H3 } from "h3";
import { Hono } from "hono";
import { Elysia } from "elysia";
// 两个第三方应用分别挂到不同前缀,互不干扰
const app = new H3()
.mount(
"/elysia",
new Elysia().get("/test", () => "Hello Elysia!"),
)
.mount(
"/hono",
new Hono().get("/test", (c) => c.text("Hello Hono!")),
);
和挂 H3 子应用有一处不同,必须记住:
挂载 Web 标准应用时,前缀会从
request.url里移除;挂 H3 子应用时不移除。
原因是 Web 应用期望自己收到「以自己为根」的请求。所以 Hono 里写 /test 路由,对应外部路径是 /hono/test,而它看到的 request.url 已经变成 /test。
反过来:把 H3 挂到别的框架上
这个能力是对称的。H3 应用本身就是一个 .fetch 处理器,可以直接挂到 Hono 或 Elysia 的路由下(各家都提供 mount)。迁移期特别有用:新老框架并存,按路径逐步切换。
该不该拆
| 情况 | 建议 |
|---|---|
| 单团队、路由量中等 | 不拆,一个实例 + 路由规则足够 |
按业务域边界清晰(如 /admin、/openapi) |
拆成子应用,各自维护中间件与路由 |
| 已有其他团队的 Web 应用 | 直接 mount,不重写 |
| 需要统一鉴权、日志、错误处理 | 拆,但钩子与全局中间件集中在主应用 |
拆分的代价要认清:中间件的作用范围变复杂了。主应用注册的 app.use("/api/**") 能覆盖挂载进去的子应用路径,但子应用内部的中间件只在子应用内生效,排查顺序问题时需要同时看两层。
排查表
| 现象 | 原因 |
|---|---|
| 子应用里的重定向跳到了错误地址 | 用了相对路径拼接,未考虑 pathname 是全路径 |
| 挂载的 Hono 应用所有路由 404 | 对方按「已剥前缀」的 URL 匹配,你按带前缀的路径注册了 |
| 子应用抛错没有日志 | 用了子应用的全局钩子,挂载后不生效,改用 onError 中间件 |
| 子应用的中间件没拦住主应用的路由 | 正常现象:子应用中间件只作用于子应用内部 |
延伸阅读
- 挂载在生命周期里的位置:见 2.1 请求生命周期
- 路径归一化与通配捕获:见 2.2 路由
- 错误钩子与中间件的取舍:见 2.6 错误处理
- 单个 handler 而非整棵应用如何转换:见 2.4 事件处理器

TRAE-AI编程


更多建议: