H3 事件处理器
事件处理器(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 嵌套应用