插件
VextJS 的插件系统是框架的唯一扩展入口。通过插件,你可以向 app 对象注入自定义能力、注册全局中间件、替换内置实现、管理资源生命周期。
基本概念
插件放在 src/plugins/ 目录下,由 plugin-loader 自动扫描加载。每个插件通过 definePlugin() 定义,包含名称、依赖声明和 setup() 初始化函数。
// src/plugins/redis.ts
import { definePlugin } from 'vextjs';
import Redis from 'ioredis';
export default definePlugin({
name: 'redis',
async setup(app) {
const redis = new Redis(app.config.redis?.url ?? 'redis://localhost:6379');
// 向 app 挂载自定义能力
app.extend('cache', redis);
// 注册优雅关闭钩子
app.onClose(async () => {
app.logger.info('Closing Redis connection...');
await redis.quit();
});
app.logger.info('Redis plugin initialized');
},
});
插件接口
interface VextPlugin {
/** 插件名称(唯一标识) */
readonly name: string;
/** 依赖的其他插件名称列表 */
readonly dependencies?: string[];
/** 插件初始化函数 */
setup(app: VextApp): Promise<void> | void;
}
name — 唯一标识
插件名称用于日志输出、错误信息和依赖声明。同名插件后加载的会覆盖先加载的,可用于替换内置实现。
dependencies — 依赖声明
声明当前插件依赖的其他插件。plugin-loader 根据依赖关系进行拓扑排序,确保依赖的插件先于当前插件执行 setup()。存在循环依赖时 Fail Fast 报错。
export default definePlugin({
name: 'user-cache',
dependencies: ['redis'], // 确保 redis 插件先初始化
async setup(app) {
// 此时 app.cache(由 redis 插件注入)已可用
const redis = (app as any).cache;
// ...
},
});
setup() — 初始化函数
插件的核心逻辑。在 bootstrap 阶段由 plugin-loader 调用,支持异步操作(如连接数据库)。每个 setup() 有超时保护(默认 30 秒),超时后自动抛出错误。
插件能力
app.extend() — 挂载自定义属性
向 app 对象注入自定义属性或方法。只有插件可以使用此 API。
export default definePlugin({
name: 'mailer',
async setup(app) {
const mailer = {
async send(to: string, subject: string, body: string) {
// 发送邮件逻辑...
app.logger.info({ to, subject }, 'Email sent');
},
};
app.extend('mailer', mailer);
},
});
使用时:
// 在路由或服务中
await (app as any).mailer.send('user@example.com', 'Welcome', 'Hello!');
类型提示
配合 declare module 获得完整的类型支持:
// src/types/extensions.d.ts
declare module 'vextjs' {
interface VextApp {
mailer: {
send(to: string, subject: string, body: string): Promise<void>;
};
}
}
扩展后 app.mailer.send() 将获得 IDE 自动补全,无需 as any 断言。
app.use() — 注册全局中间件
在插件中注册全局中间件,对所有路由生效。这些中间件在内置全局中间件之后、路由级中间件之前执行。
export default definePlugin({
name: 'security-headers',
setup(app) {
app.use(async (req, res, next) => {
await next();
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
});
},
});
注意
app.use() 只能在 setup() 中调用。路由注册完成后再调用将抛出错误。
app.onClose() — 优雅关闭钩子
注册优雅关闭钩子。当收到 SIGTERM / SIGINT 信号时,框架按注册的逆序(LIFO)执行所有关闭钩子。
适合:关闭数据库连接、刷新日志缓冲区、取消定时任务等。
export default definePlugin({
name: 'database',
async setup(app) {
const db = await createDatabaseConnection(app.config.database);
app.extend('db', db);
app.onClose(async () => {
app.logger.info('Closing database connection...');
await db.disconnect();
});
},
});
app.onReady() — 就绪钩子
注册就绪钩子。所有插件加载完成、HTTP 开始监听之后触发。适合:预热缓存、检查外部依赖、打印启动信息。
export default definePlugin({
name: 'warmup',
setup(app) {
app.onReady(async () => {
// HTTP 已开始监听,可以执行预热操作
app.logger.info('Warming up caches...');
await app.services.product.warmupCache();
app.logger.info('Cache warmup complete');
});
},
});
app.setValidator() — 替换校验引擎
替换框架内置的参数校验引擎。默认使用 schema-dsl,可替换为 Zod、Yup 等第三方校验库。
import { definePlugin } from 'vextjs';
import type { VextValidator } from 'vextjs';
export default definePlugin({
name: 'zod-validator',
setup(app) {
const zodValidator: VextValidator = {
validate(schema, data) {
// Zod 校验逻辑...
return { valid: true, data };
},
toJSONSchema(schema) {
// 转换为 JSON Schema(供 OpenAPI 使用)
return {};
},
};
app.setValidator(zodValidator);
},
});
app.setThrow() — 包装错误抛出
包装或替换 app.throw() 的实现。接收原始实现,返回新实现。
export default definePlugin({
name: 'error-tracker',
setup(app) {
app.setThrow((originalThrow) => {
return (status, message, paramsOrCode, code) => {
// 在抛出前记录错误
app.logger.warn({ status, message }, 'HTTP error thrown');
// 调用原始实现
originalThrow(status, message, paramsOrCode, code);
};
});
},
});
app.setRateLimiter() — 替换限流实现
替换内置的限流器。默认使用 flex-rate-limit,可替换为 Redis 分布式限流等。
export default definePlugin({
name: 'redis-rate-limit',
dependencies: ['redis'],
setup(app) {
app.setRateLimiter({
async check(key: string) {
// 基于 Redis 的分布式限流
const count = await (app as any).cache.incr(`ratelimit:${key}`);
if (count === 1) {
await (app as any).cache.expire(`ratelimit:${key}`, 60);
}
return {
allowed: count <= app.config.rateLimit.max,
remaining: Math.max(0, app.config.rateLimit.max - count),
resetAt: Date.now() + 60000,
};
},
});
},
});
app.setRequestIdGenerator() — 自定义请求 ID
覆盖请求 ID 的生成算法。默认使用 crypto.randomUUID()。
export default definePlugin({
name: 'custom-request-id',
setup(app) {
let counter = 0;
app.setRequestIdGenerator(() => {
// 使用自定义格式:时间戳 + 计数器
return `${Date.now()}-${++counter}`;
});
},
});
插件加载流程
启动时序
在 bootstrap 启动流程中,插件在以下阶段执行:
1. config → 加载并合并配置
2. locales → 加载 i18n 语言包
3. plugins → ⭐ 拓扑排序 + 执行 setup()(此处)
4. middlewares → 扫描中间件定义
5. services → 实例化服务
6. routes → 注册路由
7. HTTP 监听 → onReady 钩子触发
这意味着:
- ✅
setup() 中可以访问 app.config(已加载)
- ✅
setup() 中可以访问 app.logger(已初始化)
- ✅
setup() 中可以调用 app.extend() / app.use() / app.onClose() / app.onReady()
- ❌
setup() 中不能访问 app.services(服务尚未加载)
- ❌
setup() 中不能假设路由已注册
如需在所有模块加载完成后执行操作,使用 app.onReady()。
拓扑排序
plugin-loader 根据 dependencies 声明进行拓扑排序:
// plugins/database.ts — 无依赖,最先执行
definePlugin({ name: 'database', setup: ... });
// plugins/cache.ts — 依赖 database
definePlugin({ name: 'cache', dependencies: ['database'], setup: ... });
// plugins/session.ts — 依赖 cache 和 database
definePlugin({ name: 'session', dependencies: ['cache', 'database'], setup: ... });
执行顺序:database → cache → session
如果存在循环依赖(A → B → A),框架会在启动时 Fail Fast 报错。
超时保护
每个 setup() 有超时保护(默认 30 秒)。如果插件初始化超时(例如数据库连接超时),框架会抛出明确的错误信息。
实战示例
数据库插件
// src/plugins/database.ts
import { definePlugin } from 'vextjs';
export default definePlugin({
name: 'database',
async setup(app) {
// 从配置中读取数据库连接信息
const dbConfig = app.config.database ?? {
host: 'localhost',
port: 5432,
database: 'myapp',
};
// 创建数据库连接(示例)
const pool = await createPool(dbConfig);
// 注入到 app
app.extend('db', {
query: (sql: string, params?: unknown[]) => pool.query(sql, params),
transaction: (fn: Function) => pool.transaction(fn),
});
// 优雅关闭
app.onClose(async () => {
app.logger.info('Closing database pool...');
await pool.end();
});
// 就绪检查
app.onReady(async () => {
try {
await pool.query('SELECT 1');
app.logger.info('Database connection verified');
} catch (err) {
app.logger.error({ err }, 'Database health check failed');
}
});
app.logger.info('Database plugin initialized');
},
});
async function createPool(config: any) {
// 实际实现中使用 pg、mysql2 等驱动
return {
query: async (sql: string, params?: unknown[]) => ({ rows: [] }),
transaction: async (fn: Function) => fn(),
end: async () => {},
};
}
Sentry 错误监控插件
// src/plugins/sentry.ts
import { definePlugin } from 'vextjs';
export default definePlugin({
name: 'sentry',
setup(app) {
const dsn = app.config.sentry?.dsn;
if (!dsn) {
app.logger.warn('Sentry DSN not configured, skipping initialization');
return;
}
// 初始化 Sentry
// Sentry.init({ dsn });
// 注册全局错误捕获中间件
app.use(async (req, res, next) => {
try {
await next();
} catch (err) {
// 上报到 Sentry
// Sentry.captureException(err, { extra: { requestId: req.requestId } });
app.logger.error({ err, requestId: req.requestId }, 'Error captured by Sentry');
// 重新抛出,让框架的 error-handler 处理响应
throw err;
}
});
app.logger.info('Sentry plugin initialized');
},
});
定时任务插件
// src/plugins/scheduler.ts
import { definePlugin } from 'vextjs';
export default definePlugin({
name: 'scheduler',
setup(app) {
const timers: NodeJS.Timeout[] = [];
app.extend('scheduler', {
every(ms: number, name: string, fn: () => Promise<void>) {
const timer = setInterval(async () => {
try {
await fn();
} catch (err) {
app.logger.error({ err, task: name }, 'Scheduled task failed');
}
}, ms);
timers.push(timer);
app.logger.info({ name, intervalMs: ms }, 'Scheduled task registered');
},
});
// 优雅关闭时清除所有定时器
app.onClose(() => {
for (const timer of timers) {
clearInterval(timer);
}
app.logger.info(`Cleared ${timers.length} scheduled task(s)`);
});
// 就绪后注册定时任务
app.onReady(async () => {
(app as any).scheduler.every(60_000, 'cleanup-expired-sessions', async () => {
// await app.services.session.cleanupExpired();
app.logger.debug('Expired sessions cleaned up');
});
});
},
});
内置插件
VextJS 内置了以下插件:
内置插件通过 shouldLoadMonSQLize() 检测是否应加载,实现零配置零开销——未安装对应依赖时完全不加载。
插件 vs 中间件 vs 服务
选择指南:
- 需要在启动时初始化资源(如数据库连接)→ 插件
- 需要拦截每个请求(如认证检查)→ 中间件
- 需要封装可复用的业务逻辑 → 服务
- 需要向
app 添加新能力 → 插件(app.extend())
- 需要替换框架内置行为 → 插件(
app.setValidator() 等)
最佳实践
1. 条件初始化
根据配置决定是否初始化插件,避免在不需要时浪费资源:
export default definePlugin({
name: 'redis',
async setup(app) {
if (!app.config.redis?.enabled) {
app.logger.info('Redis not configured, skipping');
return;
}
// 初始化...
},
});
2. 始终注册关闭钩子
如果插件打开了外部连接(数据库、消息队列、Redis 等),必须注册 app.onClose() 钩子确保优雅关闭:
app.extend('mq', messageQueue);
app.onClose(async () => {
await messageQueue.close();
});
3. 明确声明依赖
如果插件依赖其他插件的注入能力,务必在 dependencies 中声明,而非假设加载顺序:
// ✅ 正确 — 显式声明
definePlugin({
name: 'session',
dependencies: ['redis'],
setup(app) { /* ... */ },
});
// ❌ 危险 — 依赖文件名排序
definePlugin({
name: 'session',
// 没有 dependencies,假设 redis 因为字母序在前面会先加载
setup(app) { /* ... */ },
});
4. 使用 app.onReady() 执行后置操作
需要等到所有模块加载完成再执行的操作(如预热缓存),应放在 app.onReady() 而非 setup() 中:
setup(app) {
// ❌ setup 时 services 尚未加载
// await app.services.user.warmupCache();
// ✅ onReady 时一切就绪
app.onReady(async () => {
await app.services.user.warmupCache();
});
},
5. 错误容忍
非核心插件的初始化失败不应阻塞整个应用启动:
export default definePlugin({
name: 'analytics',
async setup(app) {
try {
const client = await initAnalytics(app.config.analytics);
app.extend('analytics', client);
} catch (err) {
app.logger.warn({ err }, 'Analytics plugin init failed, continuing without analytics');
// 提供空实现,避免其他代码因 app.analytics 不存在而崩溃
app.extend('analytics', {
track: () => {},
identify: () => {},
});
}
},
});
下一步
- 了解 参数校验 的声明式 DSL 语法
- 学习 中间件 如何配合插件使用
- 查看 配置 中插件相关的配置项
- 探索 测试 如何为插件编写测试