H3 事件处理器

2026-10-30 15:38 更新

事件处理器(event handler)是 H3 里唯一必须掌握的概念:它就是一个接收 H3Event、返回响应的函数。没有基类要继承,没有固定签名要记忆。

正因为太简单,反而有三个问题需要专门说清:什么时候该用 defineHandler,handler 上还能挂什么(中间件、元信息),以及怎么把 Node.js 或别的框架写的处理器搬过来。

函数即 handler

最省事的写法就是直接传一个函数:

import { H3 } from "h3";

const app = new H3();

// 接收 event,返回响应 —— 这就够了
app.get("/", (event) => "Response");

defineHandler 是可选的,它的作用是给返回值类型和 event 类型提供推断,不改变运行时行为:

import { H3, defineHandler } from "h3";

const app = new H3();

const handler = defineHandler((event) => "Response"); // 带类型的 handler

app.get("/", handler);

同步和异步都支持,返回 Promise 时 H3 会等它 resolve 后再发响应:

// async handler:等数据库查完再返回
defineHandler(async (event) => {
  const rows = await db.query("select 1");
  return rows;
});

📌 v1 里的 eventHandler 与 defineEventHandler 已弃用,统一迁移到 defineHandler;isEventHandler 已被移除——因为现在任何函数都可以是 handler,不需要判断。

对象语法:把中间件和元信息挂上去

defineHandler 接受对象形式,一次声明三件事:中间件、元信息、处理逻辑。

import { basicAuth } from "h3";

const secretHandler = defineHandler({
  // 只随这个 handler 执行的中间件
  middleware: [basicAuth({ password: "test" })],
  handler: (event) => "Hi!",
});

元信息(meta)会随路由一起登记,任何中间件都能读到——这是把「这条路由需要管理员权限」写在路由旁边、而不是写进全局判断表的办法:

import { H3, defineHandler } from "h3";

const app = new H3();

app.use((event) => {
  // 读当前命中路由的元信息,做统一鉴权
  console.log(event.context.matchedRoute?.meta); // { tag: "admin" }
});

app.get(
  "/admin/**",
  defineHandler({
    meta: { tag: "admin" },
    handler: (event) => "Hi!",
  }),
);

注册路由时也可以直接传 meta(第三个参数),两种写法等价,取决于你习惯把选项写在路由上还是写在 handler 上(见 2.2 的路由元信息)。

handler.fetch:不装框架也能用

用 defineHandler 定义的处理器自带 .fetch 方法,可以直接当 Web handler 调用——写测试或嵌到别的运行时里时很有用:

const handler = defineHandler(async (event) => `请求地址:${event.req.url}`);

// 无需 new H3()、无需监听端口,直接喂一个 URL
const response = await handler.fetch("http://localhost/");
console.log(response.status, await response.text());

惰性处理器:只初始化一次

defineLazyEventHandler 用来放「第一次命中时才执行、之后不再执行」的准备工作:读配置、初始化客户端、加载重型依赖。它必须返回一个事件处理器。

import { defineLazyEventHandler } from "h3";

defineLazyEventHandler(async () => {
  // 只有首个匹配请求会触发这里,之后复用返回的 handler
  const db = await connectDatabase();

  return (event) => {
    return { ok: true };
  };
});

更常见的用法是按需加载代码块,让启动时不加载路由模块:

// app.mjs:把路由模块拆出去,首次访问才 import
import { H3, defineLazyEventHandler } from "h3";

const app = new H3();

app.all(
  "/route",
  defineLazyEventHandler(() => import("./route.mjs").then((mod) => mod.default)),
);

// route.mjs:被懒加载的模块,默认导出一个 handler
import { defineHandler } from "h3";

export default defineHandler((event) => "Hello!");

⚠️ 惰性初始化的代价是「首次请求变慢」。如果你的场景是冷启动敏感(比如边缘函数),更适合在模块顶层直接初始化,而不是等到第一个用户来承担。

把别人的处理器搬过来

现实项目里总有几个「已经写好、但签名不一样」的处理器。H3 给了三条转换路径。

Web 标准的 handler

签名是 Request => Response 的处理器(Hono、Elysia 等都符合),有三种接法:

import { H3, fromWebHandler } from "h3";

export const app = new H3();

const webHandler = (request) => new Response("👋 Hello!");

// 写法一:用转换工具
app.all("/web", fromWebHandler(webHandler));

// 写法二:手写一层包装,把 event.req 递进去
app.all("/web2", (event) => webHandler(event.req));

// 写法三:用 mount 挂到子路径(推荐,见 2.7)
app.mount("/web3", webHandler);

三种写法效果一致。要挂整棵子路由用 mount,只转一个处理器用 fromWebHandler。

Node.js 的 (req, res) handler

老的 (req, res) => {} 写法用 fromNodeHandler 转换:

import { H3, fromNodeHandler } from "h3";

// 强制走 Node.js 兼容层(在 Bun、Deno 下也能工作)
import { serve } from "h3/node";

export const app = new H3();

const nodeHandler = (req, res) => {
  res.end("Node handlers work!");
};

app.get("/web", fromNodeHandler(nodeHandler));

❗ 转换后的处理器只能在 Node.js 服务端运行时里跑。它依赖 req / res 的原生对象,在 Bun、Deno、Workers 下即使语法能过,行为也不保证一致。

v1 的写法对照

v1 v2
eventHandler / defineEventHandler defineHandler(或直接传函数)
lazyEventHandler defineLazyEventHandler
isEventHandler 已移除(任何函数都是 handler)
toNodeListener / fromNodeMiddleware toNodeHandler / fromNodeHandler

完整弃用清单见 8.2。

一个容易忽略的细节

handler 返回 undefined 与返回 null 效果相同——都会得到 200 + 空响应体,而不是报错。如果你期望「没数据就 404」,必须显式写:

app.get("/user/:id", (event) => {
  const user = findUser(event.context.params.id);

  // 必须显式抛错,否则客户端拿到的是 200 + 空体
  if (!user) {
    throw new HTTPError("用户不存在", { status: 404 });
  }

  return user;
});

延伸阅读

  • handler 在生命周期里的位置与中间件的先后:见 2.1 请求生命周期
  • 返回值的全部转换规则:见 2.5 发送响应
  • HTTPError 的完整字段与错误 JSON 结构:见 2.6 错误处理
  • 用 mount 挂载整个子应用:见 2.7 嵌套应用
以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号