插件 API
本页详细介绍 VextJS 的插件系统 API,包括插件定义、中间件定义辅助函数和相关类型。
概述
插件是 VextJS 框架的唯一扩展入口。通过插件可以:
- 向
app 挂载自定义属性(app.extend())
- 注册全局中间件(
app.use())
- 注册优雅关闭钩子(
app.onClose())
- 注册就绪钩子(
app.onReady())
- 替换内置实现(
app.setValidator() / app.setThrow() / app.setRateLimiter())
插件文件放在 src/plugins/ 目录下,plugin-loader 在启动时自动扫描加载。
definePlugin
definePlugin 是创建插件的推荐方式,提供类型推断和 IDE 自动补全支持。
函数签名
function definePlugin(plugin: VextPlugin): VextPlugin;
接收一个 VextPlugin 对象,原样返回(仅用于类型标注)。
基本用法
// 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);
app.extend('cache', redis);
app.onClose(() => redis.quit());
},
});
VextPlugin
插件接口定义。
interface VextPlugin {
readonly name: string;
readonly dependencies?: string[];
setup(app: VextApp): Promise<void> | void;
}
name
插件名称,全局唯一标识。
用于日志输出、错误信息和依赖声明。同名插件后加载的会覆盖先加载的(可用于替换内置实现)。
export default definePlugin({
name: 'my-plugin', // 唯一标识
setup(app) { /* ... */ },
});
dependencies
依赖的其他插件名称列表(可选)。
readonly dependencies?: string[];
plugin-loader 根据此字段进行拓扑排序,确保依赖的插件先于当前插件执行 setup()。存在循环依赖时 Fail Fast 报错。
export default definePlugin({
name: 'user-cache',
dependencies: ['redis', 'database'], // 确保 redis 和 database 先初始化
async setup(app) {
// 此时 app.cache(redis 插件挂载)和 app.db(database 插件挂载)已就绪
const userCache = new UserCacheService(app.cache, app.db);
app.extend('userCache', userCache);
},
});
Warning
循环依赖会导致启动失败:
[vextjs] Circular dependency detected: redis → database → redis
setup(app)
插件初始化函数,在 bootstrap 的步骤②被 plugin-loader 调用。
setup(app: VextApp): Promise<void> | void;
参数:
关键说明:
- 可以是同步或异步函数
plugin-loader 为每个 setup() 设置超时保护(默认 30 秒),超时后抛出错误
- 执行顺序由
dependencies 拓扑排序决定
setup() 执行时 app.services 尚未注入(service-loader 在 plugin-loader 之后执行),不能访问服务
export default definePlugin({
name: 'database',
async setup(app) {
// ✅ 可以访问 app.config
const pool = await createPool(app.config.database);
// ✅ 可以挂载自定义属性
app.extend('db', pool);
// ✅ 可以注册全局中间件
app.use(myMiddleware);
// ✅ 可以注册生命周期钩子
app.onReady(async () => {
await pool.query('SELECT 1');
app.logger.info('数据库连接验证成功');
});
app.onClose(async () => {
await pool.end();
app.logger.info('数据库连接池已关闭');
});
// ❌ 不能访问 app.services(此时尚未注入)
// app.services.user → undefined
},
});
插件加载机制
自动扫描
plugin-loader 自动扫描 src/plugins/ 目录下的所有 .ts / .js 文件,每个文件的 default export 应为 VextPlugin 对象。
src/plugins/
├── database.ts → definePlugin({ name: 'database', ... })
├── redis.ts → definePlugin({ name: 'redis', ... })
└── auth.ts → definePlugin({ name: 'auth', ... })
拓扑排序
根据 dependencies 字段自动计算执行顺序:
// database.ts — 无依赖,最先执行
definePlugin({ name: 'database', setup(app) { ... } })
// redis.ts — 无依赖,与 database 并列
definePlugin({ name: 'redis', setup(app) { ... } })
// auth.ts — 依赖 database 和 redis
definePlugin({
name: 'auth',
dependencies: ['database', 'redis'],
setup(app) { ... },
})
执行顺序:database → redis → auth
超时保护
每个 setup() 有 30 秒超时限制(默认值)。如果插件初始化时间超过此限制(如数据库连接超时),plugin-loader 将抛出错误并中止启动。
内置插件
VextJS 内置了 monsqlize 插件(数据库抽象层),通过 createMonSQLizePlugin() 创建:
import { createMonSQLizePlugin } from 'vextjs';
defineMiddleware
创建无配置中间件的辅助函数。通过 Symbol 标记确保中间件类型安全。
函数签名
function defineMiddleware(
middleware: VextMiddleware
): TaggedMiddleware;
基本用法
// src/middlewares/auth.ts
import { defineMiddleware } from 'vextjs';
export default defineMiddleware(async (req, _res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
req.app.throw(401, '未提供认证令牌');
}
try {
const decoded = await verifyJWT(token);
req.user = decoded;
} catch {
req.app.throw(401, '认证令牌无效或已过期');
}
await next();
});
VextMiddleware 类型
type VextMiddleware = (
req: VextRequest,
res: VextResponse,
next: () => Promise<void>,
) => Promise<void> | void;
三个参数:
洋葱模型
中间件通过 await next() 实现洋葱模型,可以在 handler 执行前后分别处理:
export default defineMiddleware(async (req, res, next) => {
// ── before handler(请求进入阶段)──
const start = Date.now();
console.log(`→ ${req.method} ${req.path}`);
await next(); // 执行 handler 及后续中间件
// ── after handler(响应返回阶段)──
const duration = Date.now() - start;
console.log(`← ${req.method} ${req.path} ${res.statusCode} ${duration}ms`);
});
执行流程:
请求 → 中间件A(before) → 中间件B(before) → handler → 中间件B(after) → 中间件A(after) → 响应
短路响应
不调用 next() 可以短路请求,handler 不会执行:
export default defineMiddleware(async (req, res, next) => {
// IP 黑名单检查
if (blockedIPs.has(req.ip)) {
res.status(403).json({ message: '访问被拒绝' });
return; // 不调用 next()
}
await next();
});
错误处理
中间件中抛出的错误会被框架 error-handler 统一捕获:
export default defineMiddleware(async (req, _res, next) => {
if (!req.headers.authorization) {
// 使用 app.throw 抛出标准 HTTP 错误
req.app.throw(401, '未提供认证令牌');
// 等价于 throw new HttpError(401, '未提供认证令牌')
}
await next();
});
defineMiddlewareFactory
创建带配置的中间件工厂。接收配置参数,返回中间件函数。
函数签名
function defineMiddlewareFactory<T = unknown>(
factory: (options: T) => VextMiddleware
): TaggedMiddlewareFactory;
基本用法
// src/middlewares/role.ts
import { defineMiddlewareFactory } from 'vextjs';
interface RoleOptions {
required: string | string[];
}
export default defineMiddlewareFactory<RoleOptions>((options) => {
const requiredRoles = Array.isArray(options.required)
? options.required
: [options.required];
return async (req, _res, next) => {
if (!req.user) {
req.app.throw(401, '未认证');
}
if (!requiredRoles.includes(req.user.role)) {
req.app.throw(403, '权限不足', {
required: requiredRoles.join(', '),
current: req.user.role,
});
}
await next();
};
});
配置传递
中间件工厂的配置通过 config.middlewares 白名单传递:
// src/config/default.ts
export default {
middlewares: [
{ name: 'auth' }, // 无配置中间件
{ name: 'role', options: { required: 'admin' } }, // 工厂中间件 + 配置
{ name: 'cache', options: { ttl: 300 } }, // 工厂中间件 + 配置
],
};
路由中引用时可以覆盖默认配置:
app.get('/admin/users', {
middlewares: [
'auth',
{ name: 'role', options: { required: ['admin', 'superadmin'] } },
],
}, handler);
更多示例
缓存中间件:
// src/middlewares/cache.ts
import { defineMiddlewareFactory } from 'vextjs';
interface CacheOptions {
ttl: number; // 缓存时间(秒)
keyPrefix?: string; // 缓存 key 前缀
}
export default defineMiddlewareFactory<CacheOptions>((options) => {
return async (req, res, next) => {
const cacheKey = `${options.keyPrefix ?? 'cache'}:${req.path}`;
const cached = await req.app.cache?.get(cacheKey);
if (cached) {
res.json(JSON.parse(cached));
return; // 命中缓存,不调用 next()
}
await next();
// TODO: 将响应写入缓存(需要拦截 res.json)
};
});
限速中间件:
// src/middlewares/throttle.ts
import { defineMiddlewareFactory } from 'vextjs';
interface ThrottleOptions {
max: number;
window: number; // 秒
}
export default defineMiddlewareFactory<ThrottleOptions>((options) => {
const store = new Map<string, { count: number; resetAt: number }>();
return async (req, _res, next) => {
const key = req.ip;
const now = Date.now();
const entry = store.get(key);
if (entry && now < entry.resetAt) {
if (entry.count >= options.max) {
req.app.throw(429, '请求过于频繁');
}
entry.count++;
} else {
store.set(key, {
count: 1,
resetAt: now + options.window * 1000,
});
}
await next();
};
});
isMiddleware / isMiddlewareFactory
类型检查辅助函数,用于判断一个值是否为 defineMiddleware / defineMiddlewareFactory 创建的中间件。
函数签名
function isMiddleware(value: unknown): value is TaggedMiddleware;
function isMiddlewareFactory(value: unknown): value is TaggedMiddlewareFactory;
用法
import {
isMiddleware,
isMiddlewareFactory,
} from 'vextjs';
const middlewareModule = await import('./middlewares/auth.ts');
const exported = middlewareModule.default;
if (isMiddleware(exported)) {
// 无配置中间件,直接使用
adapter.registerMiddleware(exported);
} else if (isMiddlewareFactory(exported)) {
// 工厂中间件,需要传入 options 调用后获得中间件实例
const middleware = exported(options);
adapter.registerMiddleware(middleware);
}
Tip
这两个函数通常由框架内部的 middleware-loader 使用,用户代码很少需要直接调用。
VextErrorMiddleware
错误中间件类型(框架内部使用)。
type VextErrorMiddleware = (
error: Error,
req: VextRequest,
res: VextResponse,
next: () => Promise<void>,
) => Promise<void> | void;
与普通中间件不同,错误中间件多接收一个 error 参数。框架内置的 error-handler 使用此类型。用户通常不需要直接创建错误中间件,error-handler 已提供完善的错误处理逻辑。
TaggedMiddleware / TaggedMiddlewareFactory
被 Symbol 标记的中间件类型,用于 middleware-loader 区分普通函数和框架中间件。
interface TaggedMiddleware extends VextMiddleware {
[MIDDLEWARE_SYMBOL]: true;
}
interface TaggedMiddlewareFactory {
(options: unknown): VextMiddleware;
[MIDDLEWARE_FACTORY_SYMBOL]: true;
}
Symbol 常量
import {
MIDDLEWARE_SYMBOL,
MIDDLEWARE_FACTORY_SYMBOL,
} from 'vextjs';
这些 Symbol 由 defineMiddleware / defineMiddlewareFactory 自动附加,用户代码不需要手动设置。
中间件文件组织
目录结构
src/middlewares/
├── auth.ts → defineMiddleware(...) // 认证中间件
├── role.ts → defineMiddlewareFactory(...) // 角色校验(带配置)
├── cache.ts → defineMiddlewareFactory(...) // 缓存中间件(带配置)
└── request-logger.ts → defineMiddleware(...) // 请求日志
中间件注册流程
middleware-loader 扫描 src/middlewares/ 目录
- 根据文件名和
config.middlewares 白名单匹配
- 使用
isMiddleware() / isMiddlewareFactory() 区分类型
- 工厂中间件调用
factory(options) 获取中间件实例
- 注册到中间件定义映射(
Map<string, VextMiddleware>)
- 路由注册时通过名称引用
配置白名单
只有在 config.middlewares 中声明的中间件才能在路由 options.middlewares 中引用:
// src/config/default.ts
export default {
middlewares: [
{ name: 'auth' },
{ name: 'role', options: { required: 'user' } },
],
};
未在白名单中声明的中间件在路由中引用会抛出启动错误。
内置中间件
VextJS 提供以下内置中间件,由 bootstrap 自动注册,无需手动配置:
这些中间件可以通过 config 配置其行为(参见 配置项),但不能通过 app.use() 重复注册。
执行顺序
内置中间件的执行顺序(从外到内):
请求进入
→ requestId ← 生成/透传 requestId
→ cors ← CORS 预检
→ bodyParser ← 解析请求体
→ accessLog ← 记录请求开始时间
→ rateLimit ← 速率限制检查
→ responseWrapper ← 开启出口包装
→ [全局中间件] ← 插件通过 app.use() 注册的
→ [路由中间件] ← 路由 options.middlewares 引用的
→ [validate] ← 参数校验
→ handler ← 路由处理函数
← responseWrapper ← 包装响应
← accessLog ← 记录耗时和状态码
← errorHandler ← 捕获未处理错误
响应返回
插件开发最佳实践
1. 命名规范
- 插件
name 使用 kebab-case:'my-plugin'
- 文件名与插件名一致:
src/plugins/my-plugin.ts
2. 类型声明
使用 declare module 为扩展的属性提供类型提示:
// types/vext.d.ts
declare module 'vextjs' {
interface VextApp {
cache: import('ioredis').Redis;
db: import('./db').DatabasePool;
}
interface VextRequest {
user?: {
id: string;
role: string;
};
}
interface VextConfig {
redis?: {
host: string;
port: number;
};
database?: {
connectionString: string;
};
}
}
3. 资源清理
始终在 onClose 中清理插件创建的资源:
export default definePlugin({
name: 'database',
async setup(app) {
const pool = await createPool(app.config.database);
app.extend('db', pool);
// ✅ 务必注册关闭钩子
app.onClose(async () => {
await pool.end();
app.logger.info('数据库连接池已关闭');
});
},
});
4. 错误处理
setup() 中的错误会导致启动失败。确保关键资源的初始化有错误处理:
export default definePlugin({
name: 'database',
async setup(app) {
try {
const pool = await createPool(app.config.database);
app.extend('db', pool);
} catch (err) {
app.logger.fatal({ error: err }, '数据库连接失败');
throw err; // 重新抛出,阻止启动
}
},
});
5. 可选依赖
如果插件依赖其他插件的扩展属性,但该依赖是可选的:
export default definePlugin({
name: 'user-cache',
// 不声明 dependencies,手动检查
setup(app) {
if (app.cache) {
// Redis 可用,启用缓存
app.extend('userCache', new CachedUserService(app.cache));
} else {
// Redis 不可用,降级为无缓存模式
app.logger.warn('Redis 未配置,用户缓存已禁用');
app.extend('userCache', new UserService());
}
},
});
类型导入
import {
definePlugin,
defineMiddleware,
defineMiddlewareFactory,
isMiddleware,
isMiddlewareFactory,
MIDDLEWARE_SYMBOL,
MIDDLEWARE_FACTORY_SYMBOL,
} from 'vextjs';
import type {
VextPlugin,
VextMiddleware,
VextErrorMiddleware,
VextHandler,
VextDefinedMiddleware,
VextMiddlewareFactory,
VextMiddlewareExport,
TaggedMiddleware,
TaggedMiddlewareFactory,
} from 'vextjs';