H3 中间件

2026-10-30 15:38 更新

中间件是「在路由 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 路由规则入门
以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号