codecamp

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 响应工具
H3Event 事件对象
H3 WebSocket 与 SSE
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }