H3 应用实例完全指南:从new H3()到请求分发与全局钩子
【免费下载链接】h3⚡️ Minimal H(TTP) framework built for high performance and portability项目地址: https://gitcode.com/GitHub_Trending/h31/h3
本文以 H3 框架的
H3应用类为核心,系统讲解如何创建应用实例、注册路由与中间件、挂载子应用、配置全局选项与生命周期钩子,并深入源码剖析其内部的路由匹配、中间件组合与 URL 路径规范化机制。读完本文,你将掌握H3类的全部 API 用法,并理解每个配置项背后的实现原理,能够独立构建一个多路由、带中间件与嵌套应用的高性能 HTTP 服务。
概览:H3类是服务器的核心
H3类是 H3 服务器一切能力的入口。在 src/h3.ts 中,H3继承自H3Core,后者负责事件分发、中间件组合与响应生成等底层逻辑;H3则在之上叠加了基于 Rou3 的 HTTP 路由匹配能力("~rou3"路由器)与面向开发者的链式 API。
创建一个应用实例只需要一行代码:
import { H3 } from "h3"; const app = new H3({/* optional config */});构造函数接收一个可选的全局配置对象(详见下文 H3 Options)。从源码看,H3的构造函数除了初始化路由器和绑定request方法外,还会立即执行config.plugins中注册的插件(见 src/h3.ts)。
H3Methods:从请求入口到路由注册
H3.request
H3.request是一个 fetch 兼容的函数,用于直接向应用发起请求、获取响应,非常适合做内部调用、测试或服务端渲染场景。
- 输入可以是相对路径、URL 或 Request 对象;
- 返回值为
Response的 Promise。
const response = await app.request("/"); console.log(response, await response.text());从实现看,request()方法 会先调用toRequest将输入统一转换为标准的 WebRequest,再交给内部"~request"处理。当传入相对路径时,toRequest会基于请求的host头合成完整 URL(缺失或非法时回退到localhost),并且永远使用http协议、忽略x-forwarded-proto,这是刻意为之的安全设计(见 src/utils/request.ts)。
request还支持可选的RequestInit与上下文参数:
const response = await app.request("/api", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ name: "h3" }), });H3.fetch
H3.fetch与H3.request类似,但只接受一个(req: Request)参数,以换取跨运行时的一致性。它是各平台serve(app)实际使用的底层入口:在 src/_entries/node.ts 等入口文件中,serve会把app.fetch直接作为 fetch handler 交给 srvx 服务器:
export function serve(app: H3, options?: Omit<ServerOptions, "fetch">): Server { freezeApp(app); return srvxServe({ fetch: app.fetch, ...options }); }因此,H3.fetch才是真正被运行时调用的"分发入口",而H3.request更偏向开发与测试用途。
H3.on
H3.on为指定 HTTP 方法注册路由处理器:
const app = new H3().on("GET", "/", () => "OK");方法名不区分大小写——源码中on()会将方法统一toUpperCase()后交给 Rou3 路由器(见 src/h3.ts)。它支持的可选第三个参数RouteOptions包含middleware(仅该路由生效的中间件数组)与meta(路由元数据),可用于给路由附加权限标记等自定义信息。
H3.[method]
H3.[method]是app.on(method, ...)的快捷写法,H3 内置了GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS、CONNECT、TRACE、QUERY共 10 个方法的快捷注册:
const app = new H3().get("/", () => "OK");这些快捷方法是在 src/h3.ts 中通过循环批量生成在原型上的:
for (const method of ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "CONNECT", "TRACE", "QUERY"] as const) { (H3Core as any).prototype[method.toLowerCase()] = function (route, handler, opts) { return this.on(method, route, handler, opts); }; }注意两点:
- 所有注册方法都返回
this,因此可以无限链式调用; QUERY是 RFC 10008中的说明。
H3.all
H3.all为所有 HTTP 方法注册路由处理器,内部实现为this.on("", route, handler, opts)(空方法名即通配):
const app = new H3().all("/", () => "OK");典型用法是作为兜底路由(fallback),与get/post等精确方法路由共存:
app .get("/hello", () => "GET Hello world!") .post("/hello", () => "POST Hello world!") .all("/hello", () => "Any other method!");H3.use
H3.use注册全局中间件,每次请求都会先经过它们,再进入路由处理器:
const app = new H3() .use((event) => { console.log(`request: ${event.req.url}`); }) .all("/", () => "OK");use的签名非常灵活,支持三种调用形态(见 src/h3.ts):
// 1. 仅函数(作用于所有请求) app.use((event) => { /* ... */ }); // 2. 带路径与选项 app.use("/blog/**", handler, { method: "POST", match: (event) => /* ... */ }); // 3. 传入另一个 H3 实例 —— 自动转为 mount 挂载 app.use(nestedApp);第三种形态很关键:当use收到的参数带有handler属性(即是一个 H3 实例)时,会自动委托给mount完成挂载。MiddlewareOptions支持method与match(event)两个过滤选项。中间件的完整用法(含next拦截、返回值短路行为)参见中间件指南。
H3.register
H3.register用于注册一个 H3 插件来扩展应用:
import { definePlugin } from "h3"; const logger = definePlugin((h3, _options) => { h3.use((event) => { console.log(`[${event.req.method}] ${event.url.pathname}`); }); }); app.register(logger());插件本质是"接收 H3 实例并立即执行"的函数,也可在构造时通过plugins配置项传入。注意插件总是立即注册,因此注册顺序可能影响行为。详见插件指南。
H3.handler
H3.handler是一个 H3 事件处理器,用于组合多个 H3 应用实例。它接受一个H3Event,内部完成路由查找(并把params、matchedRoute写入event.context)与分发(见 src/h3.ts):
handler(event: H3Event): unknown | Promise<unknown> { const route = this"~findRoute"; if (route) { event.context.params = route.params; event.context.matchedRoute = route.data; } return (this["~dispatch"] ??= createDispatcher(this))(event, route); }示例:嵌套应用。把子应用的handler通过withBase挂到主应用的通配路由上:
import { H3, serve, redirect, withBase } from "h3"; const nestedApp = new H3().get("/test", () => "/test (sub app)"); const app = new H3() .get("/", (event) => redirect(event, "/api/test")) .all("/api/**", withBase("/api", nestedApp.handler)); serve(app);当请求/api/test时,withBase会在调用nestedApp.handler前剥离/api前缀,使子应用在自己的命名空间内完成匹配。
H3.mount
H3.mount是更直接的子应用注册方式——以指定前缀挂载一个子应用:
const nestedApp = new H3().get("/test", () => "/test (sub app)"); const app = new H3().mount("/api", nestedApp);mount支持两类目标(见 src/h3.ts):
- H3 实例:子应用的所有路由都会带上前缀合并进主应用,子应用的全局中间件会被包装成一条带前缀守卫的中间件(不匹配前缀时直接
next(),匹配时才剥离前缀并执行,且会处理/base//evil.com这类可能被下游重定向利用的协议相对 URL 攻击面); - 任意
.fetch兼容的 Web 标准应用(如 Hono、Elysia):通过app.all(base + "/**", ...)通配转发,并把基础前缀从传给子应用的request.url中移除。
子应用的全局配置与钩子不会被继承,请一律在主应用中设置。更多细节见嵌套应用指南。
H3Options:全局配置
创建实例时可传入全局应用配置。以 src/types/h3.ts 的类型定义为准,支持以下选项:
| 选项 | 类型 | 默认行为 | 说明 |
|---|---|---|---|
debug | boolean | 关闭 | 在 HTTP 错误响应中输出调试用堆栈跟踪(生产环境有安全风险!) |
silent | boolean | 关闭 | 开启后,未处理异常的控制台错误将不再打印 |
allowMalformedURL | boolean | 关闭 | 允许畸形百分号编码的 URL 路径(如/foo%、/%ZZ)以原始路径名通过,而不是默认的400 Bad Request拒绝 |
plugins | H3Plugin[] | 空 | 构造时立即注册的插件数组 |
const app = new H3({ debug: false, silent: false, allowMalformedURL: false, plugins: [logger()], });[!IMPORTANT] 开启
debug选项会在错误响应中泄露堆栈跟踪等重要信息,仅应在开发环境启用。
allowMalformedURL的底层实现值得展开:在 src/h3.ts 的"~request"方法中,H3 会在任何路由匹配或应用逻辑执行之前检查事件是否携带畸形 URL 标记,若是则直接抛出400 Bad Request:
if ((event as any)[kMalformedURL] && !this.config.allowMalformedURL) { throw new HTTPError({ status: 400, message: "Bad Request" }); }该标记由H3Event构造函数在解析 URL 时打上(见 src/event.ts):当路径包含%且无法通过decodeURI解码(如/foo%、/%ZZ、/%80这类截断/非十六进制/非法 UTF-8 转义)时置位。畸形路径没有可解码的规范形式,因此被提前拒绝。
全局钩子(Global Hooks)
初始化应用时,可以注册三个全局钩子,它们会在每个请求的生命周期中被调用:
onError—— 请求处理抛出错误时触发;onRequest—— 请求分发前触发;onResponse—— 响应生成后触发。
const app = new H3({ onRequest: (event) => { console.log("Request:", event.req.url); }, onResponse: (response, event) => { console.log("Response:", event.url.pathname, response.status); }, onError: (error, event) => { console.error(error); }, });从 src/h3.ts 的源码可以看到onRequest的执行时机——它在H3Event创建之后、handler()分发之前被调用,且支持返回 Promise 以异步化:
if (this.config.onRequest) { const hookRes = this.config.onRequest(event); handlerRes = typeof hookRes?.then === "function" ? hookRes.then(() => this.handler(event)) : this.handler(event); } else { handlerRes = this.handler(event); }而onResponse与onError则由toResponse在响应准备阶段调用(toResponse(handlerRes, event, this.config),见 src/h3.ts)。
[!IMPORTANT] 全局钩子只在主 H3 应用中运行,子应用(sub-app)不会继承。需要更灵活的逻辑时,请使用中间件。
全局钩子与生命周期
理解这几个钩子与中间件、路由处理器的先后关系,可以帮助你决定"逻辑该放哪"。H3 的完整请求生命周期(详见生命周期指南)如下:
- 接收请求:运行时调用
app.fetch(request); - 创建事件:
new H3Event(request)初始化事件(含 URL 规范化),随后调用onRequest(event)钩子,再进入app.handler(event); - 分发请求:基于
request.url与request.method匹配路由,依次执行全局中间件、路由级中间件,最后执行命中的路由处理器; - 发送响应:将返回值与准备好的响应头转换为
Response,调用onResponse(response, event)钩子,返回给运行时。
H3Properties:读取应用配置
H3.config
H3.config暴露当前 H3 实例的全局配置对象(即构造时传入的选项,见 src/h3.ts 的readonly config)。它在插件与中间件中尤为常用——例如插件可以根据h3.config.debug决定是否启用日志(参考插件指南中的definePlugin示例)。
const app = new H3({ debug: true }); // 在插件或中间件中读取 app.use((event) => { if (event.app?.config.debug) { console.log(`[${event.req.method}] ${event.url.pathname}`); } });源码纵深:H3 的内部分发机制
理解了全部公开 API 后,我们来看 H3 是如何把"注册"变成"分发"的,这能帮助你更好地预测复杂场景下的行为。
1. 路由匹配与 HEAD 回退
H3使用 Rou3 作为路由匹配引擎。路由注册时,"~addRoute"会把方法 + 规范化后的路由模式加入 Rou3 树(见 src/h3.ts)。匹配时("~findRoute",见 src/h3.ts),如果当前方法未命中且方法是HEAD,会自动回退查找同路径的GET路由(遵循 RFC 9110 的 HEAD 语义),同时保留HEAD方法名以便响应阶段剥离 body:
if (match === undefined && _event.req.method === "HEAD") { return findRoute(this["~rou3"], "GET", _event.url.pathname); }因此你无需为 HEAD 单独注册处理器,app.get("/hello")即可同时响应HEAD /hello(返回相同状态码与响应头,但 body 为空)。显式注册的app.head()路由永远优先于自动回退。
2. 中间件预组合(precomposition)与缓存失效
为了性能,H3 会对中间件链做预组合:首次请求时(或use()/mount()使缓存失效后)把全局中间件一次性composeMiddleware成一个函数并缓存在"~composed"上,之后每个请求直接调用组合结果,不再逐条遍历(见 src/h3.ts)。同理,路由级中间件 + 处理器也会在首次命中时组合并缓存在路由对象上("~composed",见routeHandler)。
两个值得注意的细节:
- 若子类或实例覆写了
"~getMiddleware"(例如 Nitro 这类基于 H3 的上层框架),预组合会退化为逐请求动态组合(兼容路径); use()和mount()都会把"~dispatch"/"~composed"置为undefined以强制重新组合。
3. URL 路径规范化:为何/%61dmin无法绕过/admin
H3Event构造函数在创建事件时(src/event.ts)会对 URL 路径做一次规范化处理:所有"无谓转义"(解码后字符在 WHATWG 路径序列化中原样保留,且不是%2f/%5c/%25的转义)都会被解码。这是为了消除"下游解码消费者看到的路径"与"H3 匹配器看到的路径"之间的差异——否则/%61dmin可以绕过/admin守卫却仍到达/admin路由。
具体的规范化规则与表格见 H3Event 文档 的 "Pathname encoding" 一节,核心结论:
| 请求 | event.url.pathname | 原因 |
|---|---|---|
/%61dmin | /admin | 无谓转义(未保留字符),解码 |
/a%2eb | /a.b | 无谓转义,解码 |
/x%2fy | /x%2fy | 分隔符,必须保持编码 |
/100%25 | /100%25 | 解码会暴露嵌套转义 |
/a%20b | /a%20b | URL 序列化器本就会重新编码空格 |
需要强调的安全准则:永远不要自己解码event.url.pathname——解码可能重新引入路由和中间件从未见过的/或..,一旦该值进入文件系统或上游 URL 就是路径穿越向量。读取解码后的路由参数请使用getRouterParams(event, { decode: true });做作用域检查请使用resolveDotSegments。而畸形编码(如/foo%)没有可解码的规范形式,默认在任何处理器运行前以400 Bad Request拒绝——除非你显式设置allowMalformedURL。
4. 响应准备
"~request"最后把处理器返回值交给toResponse:它会根据返回值类型(字符串、对象、Response、ReadableStream等)生成最终Response,应用event.res中准备好的状态码与响应头(详见响应指南),并调用onResponse钩子。任何同步抛错或异步 reject 都会被捕获并转换为HTTPError响应(在debug开启时附带堆栈)。
实战组合示例
最后,把本文的核心 API 组合成一个完整应用,覆盖路由注册、中间件、钩子、嵌套挂载与请求自测:
import { H3, serve } from "h3"; // 子应用:独立的路由与中间件 const api = new H3() .use((event) => { event.res.headers.set("x-api", "1"); }) .get("/test", () => "/api/test (sub app)"); // 主应用 const app = new H3({ debug: process.env.NODE_ENV === "development", onRequest: (event) => { console.log(`Request: ${event.req.method} ${event.url.pathname}`); }, onResponse: (response, event) => { console.log(`Response: ${event.url.pathname} → ${response.status}`); }, }) .use((event) => { console.log(`[middleware] ${event.req.url}`); }) .get("/", () => "OK") .post("/", async (event) => { const body = await event.req.json(); return { received: body }; }) .mount("/api", api); // 内部自测:无需启动服务器即可验证路由 const res = await app.request("/api/test"); console.log(await res.text()); // /api/test (sub app) // 启动服务器(Node / Bun / Deno / Cloudflare 等各平台入口一致) serve(app);H3类是理解整个框架的钥匙:方法层(on/get/all/use/mount/register)负责声明式地组装应用,选项层(debug/silent/allowMalformedURL/plugins/全局钩子)控制分发行为与错误处理策略,而H3Core的分发管线则保证了中间件预组合、HEAD 回退与路径规范化这些底层语义的一致性。掌握这些,你就掌握了 H3 从请求到响应的全部脉络。
【免费下载链接】h3⚡️ Minimal H(TTP) framework built for high performance and portability项目地址: https://gitcode.com/GitHub_Trending/h31/h3
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考