H3 插件
插件是 H3 里唯一「一次性改装配」的机制:它在应用初始化时拿到整个 H3 实例,可以往里加路由、加中间件、读配置。和中间件的区别很干脆——插件在启动时跑一次,中间件在每个请求上跑一次。
插件就是一个函数
类型定义只有一行:
// 插件签名:接收 H3 实例,返回 void
type H3Plugin = (h3: H3) => void;
所以最朴素的插件就是直接写一个函数,塞给 register 或构造参数:
import { H3 } from "h3";
// 一个把健康检查路由加进来的插件
const healthPlugin = (app: H3) => {
app.get("/health", () => ({ ok: true, uptime: process.uptime() }));
};
// 两种注册方式完全等价
const app1 = new H3({ plugins: [healthPlugin] });
const app2 = new H3().register(healthPlugin);
注册是立即执行的,所以顺序很重要
官方文档唯一强调的点就在这里:插件不会被排队,register() 调用的那一刻就跑完了。这带来两个直接后果。
第一,插件注册的中间件顺序由调用顺序决定。先注册的插件中间件先执行:
import { H3 } from "h3";
const app = new H3()
// 先注册的日志中间件会包住后面的所有中间件
.register((app) => app.use((event) => console.log("outer")))
.register((app) => app.use((event) => console.log("inner")))
.get("/", () => "ok");
// 访问 / 时输出:outer → inner
第二,构造函数里的 plugins 数组也是按顺序同步执行完的,所以它内部注册的路由排在你后续 .get() 之前。
⚠️
register()是同步调用,插件函数返回的 Promise 会被丢掉。写成register(async (app) =& { await db.connect() })不会报错,但连上数据库之前请求就可能进来。异步初始化请放在中间件里做懒加载,或包成只执行一次的中间件(8.1 有对应的写法对比)。
definePlugin:只是为了类型
手写函数的类型是全宽的,读者不知道这个插件接不接参数。definePlugin 把「配置项类型」提前固定住,并返回一个工厂函数:
import { H3, definePlugin } from "h3";
// 泛型参数就是插件的选项类型
const logger = definePlugin<{ prefix?: string }>((app, options) => {
// 只在 debug 模式下装日志中间件
if (app.config.debug) {
app.use((event) => {
console.log(`${options?.prefix ?? "[h3]"} ${event.req.method} ${event.url.pathname}`);
});
}
});
const app = new H3().register(logger({ prefix: "[api]" }));
它的实现只有三行:definePlugin(def) 返回 (options) => (h3) => def(h3, options)。也就是说,definePlugin 不改变行为,只改变类型:返回值仍然是 H3Plugin,仍然可以直接放进 plugins 数组。
选项类型可选时(undefined extends T),工厂函数的参数可以省略,这也是为什么 logger() 和 logger({...}) 都能写。
插件适合做哪三件事
| 场景 | 为什么适合插件 | 例子 |
|---|---|---|
| 批量注册路由 | 只需启动时跑一次 | 把 routes/ 目录下的模块挂成 /api/* |
| 往实例上加能力 | 拿到的是完整 H3 实例 |
注入 app.get("/health")、预置 use |
| 读配置做条件装配 | 能读 app.config |
只在 debug 时挂日志、只在该环境挂 mock 路由 |
反面清单同样清楚:按请求做判断的逻辑不该写在插件函数里。插件函数的参数只有 app,没有 event;要让逻辑落到每个请求上,必须由插件去注册中间件——此时真正的判断在中间件里,插件只负责「装配」。下面这个插件就是这个分工:装配时把 base 固定下来,请求时由中间件读它。
import { H3, type H3Plugin } from "h3";
// 把「装配期确定的信息」带进请求作用域:base 在插件执行时固定,之后每个请求只读
export const withApiBase = (base: string): H3Plugin => (app) => {
app.use("/api/**", (event) => {
// 每个请求都能读到同一个 base,不需要在 handler 里重复传参
event.context.apiBase = base;
});
};
const app = new H3().register(withApiBase("/v2"));
插件里能做的事,和不能做的事
| 可以做 | 不可以做 |
|---|---|
app.get/post/... 注册路由 |
读 event(插件作用域里没有) |
app.use 注册中间件 |
让 register() 等异步结果 |
app.register 串联其他插件 |
运行时改 app.config 并指望立即生效 |
读 app.config 做条件装配 |
依赖「插件一定在某个插件之前执行」而不看调用顺序 |
最后一条值得展开:app.config 是构造时的只读引用,插件读它没问题;但插件改它是无效的——分发函数和中间件组合在 use()、mount() 时才重建,直接改配置对象不会触发重建。要按配置分支,请在注册时就判断完,别指望运行期。
排查表
| 现象 | 最可能原因 | 处理 |
|---|---|---|
| 插件里的路由被后面的中间件挡住 | 插件注册的路由在中间件之前就定型了 | 把 register 放到中间件注册之后,或让中间件自己判断路径 |
插件用了 await,但数据没准备好 |
register() 丢弃了返回的 Promise |
改成中间件里做一次懒加载 |
logger() 报参数必填 |
选项类型不可选(T 不含 undefined) |
给选项字段加 ?,或调用时显式传 {} |
插件里改 config.debug 没生效 |
配置只在构造与重建时读取 | 在注册时分支,不要运行期改 |
| 同一插件被执行两次 | 既放在 plugins 又调了 register |
保留一处即可 |
延伸阅读
- 插件与中间件在生命周期里的位置:见 2.1 请求生命周期
- 中间件的注册范围与执行顺序:见 2.3 中间件
register与构造参数的关系:见 4.1 应用实例 API- 用
onDispose在请求结束后清理插件申请的资源:见 6.4 响应工具

TRAE-AI编程


更多建议: