H3 中间件
中间件是「在路由 handler 之前插手每个请求」的机制。它能拦住请求、改掉响应、接住错误——灵活,但也最容易把应用写成没人敢动的顺序依赖。
所以官方文档在正文第一段就放了警告:能用可组合的工具函数就不要用全局中间件。这句话不是客套,后面第 3 章的路由规则就是为了消掉一批本该用配置解决的中间件。
注册一个全局中间件
用 app.use() 注册,它对每个请求都生效:
// 最简单的中间件:不返回值 → 不短路,请求继续往下走
app.use((event) => {
console.log(event.url.pathname);
});
注意这里没有 return,也没有调用 next()。按生命周期的规则,返回 undefined 就意味着「我不处理,交给下一个」。
限定路径与条件
带路径参数的 app.use() 只对匹配的请求生效:
// 只对 /blog 下的 POST 请求生效
app.use(
"/blog/**",
(event, next) => {
console.log("[alert] POST request on /blog paths!");
},
{
method: "POST",
// 等价写法:用 match 函数自定义判定
// match: (event) => event.req.method === "POST",
},
);
第三个参数是中间件选项,method 与 match 二选一即可。match 更灵活:需要判断请求头、查询参数或用户身份时用它。
⚠️ 路径必须写到
/blog/**。只写/blog的话仅匹配/blog这一条路径本身,/blog/a不会被拦住——这是 v2 与 v1 最容易踩的行为差异,app.use不再隐式匹配全部子路径。
用 next() 拦截返回值
中间件加 next 参数后,可以等后续处理完成,再对返回值动手(下沉与上浮的完整时序见 2.1 的中间件时序图):
app.use(async (event, next) => {
const startedAt = performance.now();
// 交给下一个中间件或路由 handler,拿到它的返回值
const rawBody = await next();
// 响应回来之后再做点事:这里只记耗时
event.res.headers.set("x-response-time", `${(performance.now() - startedAt).toFixed(1)}ms`);
return rawBody; // 必须把原值返回去,否则响应体就没了
});
await next() 之后的那段代码,就是「洋葱的出程」。要改响应头得在 return 之前改,改完把 rawBody 原样返回。
返回即短路
这是中间件唯一需要精确掌握的规则:
中间件返回的值只要不是
undefined、也不是next()的结果,就会立刻中断处理并直接作为响应。
// 第一个中间件总是返回 → 后面两个永远不会执行
app
.use(() => "Middleware 1")
.use(() => "Middleware 2")
.get("/", "Hello");
// 任何请求 / 都会拿到 "Middleware 1"
短路在实现「拦截类」逻辑时非常有用,例如鉴权失败直接回 401,不必让请求继续走完:
import { HTTPError } from "h3";
app.use("/admin/**", (event) => {
const token = event.req.headers.get("authorization");
if (!token) {
// 直接返回 = 短路,后面的 handler 不会执行
throw new HTTPError("请先登录", { status: 401 });
}
});
💡 这里用
throw而不是return一个错误对象:抛出能被更外层的错误处理统一接住,返回则等于「这件事到此为止,没人能再插手」。
只跟某条路由绑定的中间件
如果中间件只服务一个接口,别注册成全局的,直接写在路由选项里:
import { basicAuth } from "h3";
app.get(
"/secret",
(event) => {
return { secret: "只有带对的密码才看得到" };
},
{
// 只在这条路由上生效,不影响其他接口
middleware: [basicAuth({ password: "test" })],
},
);
这样中间件的生效范围与路由定义长在一起,阅读时不需要回头翻全局注册表。
三个工厂函数:onRequest / onResponse / onError
手写 next 链条容易出错,H3 提供了三个工厂函数把常见场景包好:
import { onRequest, onResponse, onError } from "h3";
// 1. 请求进入时
app.use(
onRequest((event) => {
console.log(`[${event.req.method}] ${event.url.pathname}`);
}),
);
// 2. 响应生成后(此时能拿到 status)
app.use(
onResponse((response, event) => {
console.log(`[${event.req.method}] ${event.url.pathname} ~>`, response.status);
}),
);
// 3. 出错时(不影响原错误继续向上传播)
app.use(
onError((error, event) => {
console.log(`[${event.req.method}] ${event.url.pathname} !! ${error.message}`);
}),
);
注意 onError 的参数顺序是 (error, event),且它只做「观察」——写日志、上报、埋点。要改错误内容请用 HTTPError,要让错误停下则需在 handler 层处理(见 2.6)。
用中间件还是用工具函数
| 场景 | 建议 |
|---|---|
| 加 CORS 头 | 优先用 routeRules 的 cors 配置,或 handleCors 工具 |
| 加缓存头 | 优先用 routeRules 的 cache,或 handleCacheHeaders |
| 静态资源 | 直接 serveStatic,不必包中间件 |
| 鉴权 | 中间件合理(逻辑是自定义的) |
| 请求日志、请求 ID | 中间件合理 |
| 响应头统一加工 | 中间件合理,但注意只改必要的几个 |
判断标准很简单:如果这件事能用「声明」表达(配一组头、开一个开关),就别用「流程」表达。声明式配置可以静态分析、可以合并、可以按路径批量生效;流程式中间件只能靠执行顺序。
常见误区
| 误区 | 实际行为 |
|---|---|
| 以为中间件不返回会报错 | 返回 undefined 表示继续,不报错 |
以为 app.use("/path") 会匹配子路径 |
只匹配 /path 本身,子路径要写 /path/** |
在 await next() 后忘记 return rawBody |
响应体丢失,客户端拿到空内容 |
以为中间件里改的 event.res.headers 在抛错后仍生效 |
抛错时准备头会被丢弃,需用 HTTPError({ headers }) |
| 全局注册大量中间件 | 顺序隐式依赖注册顺序,应用变大后难维护 |
延伸阅读
- 中间件在生命周期里的确切位置:见 2.1 请求生命周期
- 路由级
middleware选项与 handler 对象语法:见 2.4 事件处理器 - 抛错后响应头为什么会消失:见 2.5 发送响应、2.6 错误处理
- 用配置替代重复中间件:见 3.1 路由规则入门

TRAE-AI编程


更多建议: