H3 中间件完全指南:用app.use拦截请求、响应与错误
【免费下载链接】h3⚡️ Minimal H(TTP) framework built for high performance and portability项目地址: https://gitcode.com/GitHub_Trending/h31/h3
中间件是 H3 中拦截请求、响应和错误的核心机制:它在每次请求到达路由处理器之前以包装器(wrapper)形式执行,通过
next()穿透整个调用链。读完本文你将掌握全局中间件与路由级中间件的注册方式、next()的洋葱模型语义、基于路由/方法/自定义条件的匹配过滤,以及onRequest、onResponse、onError三个内置工厂函数的实战用法,并深入理解 src/middleware.ts 中的底层调度实现。
中间件在 H3 中的定位
H3 的中间件是一段在每个请求上、于路由处理器之前运行的函数,作为包装器拦截请求、响应和错误。在请求生命周期中,3. Dispatch Request阶段正是中间件发挥作用的位置:H3 根据request.url和request.method匹配路由,依次调用全局中间件,最后才调用匹配到的路由处理器。
从类型定义看,一个中间件就是接收event和next两个参数的函数(src/types/handler.ts):
export type Middleware = ( event: H3Event, next: () => MaybePromise<unknown | undefined>, ) => MaybePromise<unknown | undefined>;event:当前请求的 H3Event 实例,包含event.req、event.url、event.context等;next:调用下一个中间件(或最终的路由处理器),返回其原始返回值;- 返回值:可以是任意值(将作为响应体发送),也可以是
undefined或next()的结果(表示继续传递)。
[!IMPORTANT] H3 官方强烈建议:尽可能优先使用组合式工具函数(composable utilities),全局中间件会让应用逻辑变得不那么可预测、更难以理解。中间件应当用于横切关注点(日志、鉴权、限流等),而不是替代业务逻辑。
使用app.use注册全局中间件
全局中间件通过H3.use注册到应用实例上(见 H3 API 参考 与 src/h3.ts 的实现)。use支持两种调用形式:
use(route: string, handler: Middleware | H3, opts?: MiddlewareOptions): this; use(handler: Middleware | H3, opts?: MiddlewareOptions): this;基础示例:记录每个请求
import { H3 } from "h3"; const app = new H3(); app.use((event) => { console.log(event); });这里中间件只接收event一个参数(不声明next),执行完毕后返回undefined,H3 会继续调用下一个中间件/路由处理器,因此它纯粹是一个"旁路观察者"。
匹配特定请求
use的第二个重载允许传入路由前缀和选项对象,实现按route(路由)、method(HTTP 方法)、match(自定义函数)三种条件的组合匹配:
app.use( "/blog/**", (event, next) => { console.log("[alert] POST request on /blog paths!"); }, { method: "POST", // match: (event) => event.req.method === "POST", }, );选项类型定义在 src/types/h3.ts:
export type MiddlewareOptions = { method?: string; match?: (event: H3Event) => boolean; };route:路由模式(支持/blog/**这类通配符,与路由注册使用同一套rou3匹配引擎和normalizeRoute规范化逻辑);method:限定 HTTP 方法,源码中会通过.toUpperCase()统一大小写后比较(src/middleware.ts);match:完全自定义的谓词函数,返回false则跳过该中间件。
一个值得注意的细节:GET作用域的中间件同样会命中HEAD请求。这是因为 HEAD 由 GET 处理器提供服务(RFC 9110),src/middleware.ts中的匹配器显式处理了这一情况:
// HEAD is served by GET handlers (RFC 9110), so GET-scoped middleware also matches HEAD if (reqMethod !== method && !(method === "GET" && reqMethod === "HEAD")) { return false; }该行为在 test/middleware.test.ts 中有对应测试用例"GET-scoped global middleware also runs for HEAD requests"验证。
另外,当路由匹配成功时,路由中的参数(如/user/:id的id)会被合并进event.context.middlewareParams(src/middleware.ts),中间件内可通过event.context.middlewareParams读取,测试用例"exposes rou3 param names in middlewareParams"同样覆盖了这一点。
三种匹配条件的执行优先级
从createMatcher的实现(src/middleware.ts)可以还原匹配顺序:
- 先比较
method(不匹配直接返回false); - 再执行自定义
match回调(返回false则跳过); - 最后用
createRouteMatcher匹配路由路径,并把捕获的参数写入event.context.middlewareParams。
三个条件在use时通过选项对象任意组合,全部满足才会执行中间件。
next()与响应拦截:洋葱模型的核心
当中间件声明第二个参数next时,它就可以拦截下一个中间件和路由处理器的返回值:
app.use(async (event, next) => { const rawBody = await next(); // [intercept response] —— 在这里可以观察/改写响应体 return rawBody; });这种写法形成了经典的"洋葱模型":请求依次穿过外层中间件进入内层处理器,返回值再原路逐层返回。你可以在await next()之后统一加工响应(例如给 JSON 响应包一层结构、统计耗时、注入公共字段等)。
返回值语义:什么情况下会立即响应
[!IMPORTANT] 如果中间件返回了除
undefined或next()结果之外的值,它会立即拦截请求处理并发送响应,后续中间件和路由处理器不再执行。
app .use(() => "Middleware 1") .use(() => "Middleware 2") .get("/", "Hello");上面的链式注册中,第一个中间件直接返回字符串"Middleware 1"——这不是undefined也不是next()的结果——因此请求被立即拦截,无论请求什么路径,响应永远是Middleware 1。注意use支持链式调用(返回this)。
源码层面,这一语义由 src/middleware.ts 的callLayer实现:中间件返回值ret若为undefined或内部哨兵值kNotFound(Symbol.for("h3.notFound"),定义于 src/response.ts),则视为"未处理",自动调用next()继续;否则直接作为响应返回:
const ret = fn(event, next); return isUnhandledResponse(ret) ? next() : /* Promise 则等待解析后再判断 */ ...同时,next具备幂等保护:nextCalled标志确保next()在同一个中间件内最多执行一次,重复调用会返回首次调用的结果(src/middleware.ts),避免破坏调用链。
路由级中间件:只随特定路由运行
全局中间件作用于所有请求;当添加路由时,你也可以注册只在该路由上运行的中间件。H3.on/H3.get等方法接受第三个参数RouteOptions(src/types/h3.ts):
export type RouteOptions = { middleware?: Middleware[]; meta?: H3RouteMeta; };将中间件数组传入middleware字段即可:
import { basicAuth } from "h3"; app.get( "/secret", (event) => { /* 受保护的路由处理器 */ }, { middleware: [basicAuth({ password: "test" })], }, );这里basicAuth是 H3 内置的组合式认证工具(源码见 src/utils/auth.ts),支持username、password、validate(自定义校验函数)、realm(认证域,默认"auth")等选项;校验失败时会抛出 401 相关错误。此外也可以使用defineHandler在处理器内部声明middleware字段(src/types/handler.ts)。
从执行顺序看,请求会先经过所有全局中间件,再进入路由级中间件,最后到达处理器本身。H3Core["~getMiddleware"](src/h3.ts)拼接了两者:
"~getMiddleware"(_event, route): Middleware[] { const routeMiddleware = route?.data.middleware; const globalMiddleware = this["~middleware"]; return routeMiddleware ? [...globalMiddleware, ...routeMiddleware] : globalMiddleware; }而routeHandler(src/h3.ts)会在路由首次被命中时,用composeHandler把路由级中间件与处理器预组合成一个整体并缓存在路由对象的"~composed"字段上,后续请求直接复用。
test/middleware.test.ts 的用例完整验证了执行顺序:
(event) > async (event) > async (event, next) > async (event, next) (passthrough) > (event, next) > route (register) > route (define)(先是 5 个全局中间件按注册顺序执行,然后是注册路由时传入的middleware,最后是defineHandler内声明的middleware。)
内置工厂函数:onRequest、onResponse、onError
为方便起见,H3 提供了三个中间件工厂函数(导出自 src/utils/middleware.ts,并由 src/index.ts 统一导出):
import { onRequest, onResponse, onError } from "h3"; app.use( onRequest((event) => { console.log(`[${event.req.method}] ${event.url.pathname}`); }), ); app.use( onResponse((response, event) => { console.log(`[${event.req.method}] ${event.url.pathname} ~>`, response.status); }), ); app.use( onError((error, event) => { console.log(`[${event.req.method}] ${event.url.pathname} !! ${error.message}`); }), );onRequest:请求进入时
签名:onRequest(hook: (event: H3Event) => MaybePromise<void>): Middleware。它在每个请求到达时异步执行钩子,不参与响应拦截,适合记录访问日志、注入请求上下文。
onResponse:响应生成后
签名:onResponse(hook: (response: Response, event: H3Event) => unknown): Middleware。它内部先await next()拿到原始返回值,再经toResponse转换为Response对象,把(response, event)交给钩子;钩子若返回新的 Response 即可替换原响应,否则沿用原响应(src/utils/middleware.ts):
return async function _onResponseMiddleware(event, next) { const rawBody = await next(); const response = await toResponse(rawBody, event); const hookResponse = await hook(response, event); return hookResponse || response; };onError:错误发生时
签名:onError(hook: (error: HTTPError, event: H3Event) => unknown): Middleware。它包裹next(),捕获链中抛出的任何错误,规范化为HTTPError(非 HTTP 错误会标记error.unhandled = true并保留原始堆栈),再交给钩子;钩子返回非undefined的值即可优雅处理错误(如返回自定义错误响应),否则重新抛出(src/utils/middleware.ts)。
完整的可运行示例可参考 examples/middleware.mjs,它在一个debug: true的 H3 应用上同时挂载了三个工厂中间件,并对/error路由抛出的 500 错误做了日志记录。
与全局 Hooks 的区别
onRequest/onResponse/onError与new H3({ onRequest, onResponse, onError })配置中的全局 Hooks 功能相似,但有一个关键差异:全局 Hooks 只在主 H3 应用上运行,不会在挂载的子应用中生效(见 H3 API 参考 的说明)。而中间件可以随app.use注册在任何层级,组合更灵活——这也正是官方推荐用中间件实现全局逻辑的原因。另外,utils/middleware.ts还提供了bodyLimit(limit)中间件,可在读取请求体时强制执行字节数限制,超限在消费时表现为413错误。
深入底层:中间件链的调度与性能设计
了解背后的实现可以帮你写出更高效的中间件。H3 在 src/middleware.ts 中做了三处关键优化:
1.normalizeMiddleware的快路径。如果中间件没有声明route/method/match匹配条件,且它是异步函数或声明了两个参数(需要next),则直接原样返回,不做任何包装(src/middleware.ts);只有带匹配条件的一参数中间件才被包装为"不匹配则调用next()"的形式。
2. 预组合(precomposition)。composeMiddleware在注册阶段就把整条中间件列表编译成一个单函数调用链,把每层的派发成本从"每个请求"摊薄到"每次列表变更";请求到来时只需执行一次编译好的链。该缓存(app["~composed"]、app["~dispatch"])会在use()和mount()时主动失效重建(src/h3.ts)。测试与基准位于 test/bench/。
3.toMiddleware的互操作性。H3 还能把任意HTTPHandler(包括带fetch方法的对象、Hono 等第三方框架应用)转换成中间件:若其返回404状态码的Response,则自动继续调用next(),实现"未命中则回退"的链式挂载(src/middleware.ts)。test/middleware.test.ts 展示了将 Hono 应用挂载为 H3 中间件的用法。
最佳实践小结
- 优先组合式工具:能写在处理器/
defineHandler内部或使用basicAuth、bodyLimit等内置工具完成的逻辑,尽量不写全局中间件; - 只做横切关注点:日志、鉴权、限流、响应包装、错误兜底是中间件的天然场景;
- 拦截要显式:中间件只要返回非
undefined/非next()结果的值就会立即短路响应,务必确认这是预期行为; - 善用匹配条件:
route+method+match三条件组合可以把中间件精确限定到需要的请求子集,减少无关开销; - 区分 hooks 与 middleware:全局 Hooks 在主应用生效但子应用不继承,需要覆盖挂载子应用的场景时改用中间件。
【免费下载链接】h3⚡️ Minimal H(TTP) framework built for high performance and portability项目地址: https://gitcode.com/GitHub_Trending/h31/h3
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考