中间件
VextJS 的中间件采用 洋葱模型(Onion Model),支持请求前处理和响应后处理。框架提供 defineMiddleware 和 defineMiddlewareFactory 两种定义方式,通过约定式目录自动扫描加载。
洋葱模型
中间件通过 await next() 调用下一个中间件。next() 返回后可以执行后置逻辑,形成洋葱状的执行流程:
请求 → [中间件A-前] → [中间件B-前] → [Handler] → [中间件B-后] → [中间件A-后] → 响应
import type { VextMiddleware } from 'vextjs';
const timing: VextMiddleware = async (req, res, next) => {
// ── 前置逻辑(请求进入时执行)──
const start = Date.now();
await next(); // 执行下一个中间件 / 最终 handler
// ── 后置逻辑(响应返回时执行)──
const ms = Date.now() - start;
res.setHeader('X-Response-Time', `${ms}ms`);
req.app.logger.info(`${req.method} ${req.path} → ${res.statusCode} (${ms}ms)`);
};
中间件签名
type VextMiddleware = (
req: VextRequest,
res: VextResponse,
next: () => Promise<void>,
) => Promise<void> | void;
定义中间件
中间件文件放在 src/middlewares/ 目录下,由 middleware-loader 自动扫描。文件名即中间件名称。
普通中间件 — defineMiddleware
不需要配置参数的中间件,使用 defineMiddleware 标记:
// 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, 'Authorization token is required');
}
// 验证 token(示例)
try {
const payload = verifyJWT(token);
(req as any).user = payload;
} catch {
req.app.throw(401, 'Invalid or expired token');
}
await next();
});
function verifyJWT(token: string) {
// JWT 验证逻辑...
return { id: '1', role: 'user' };
}
工厂中间件 — defineMiddlewareFactory
需要运行时配置参数的中间件,使用 defineMiddlewareFactory 标记。工厂函数接收 options 参数,返回一个 VextMiddleware:
// src/middlewares/check-role.ts
import { defineMiddlewareFactory } from 'vextjs';
interface CheckRoleOptions {
roles: string[];
}
export default defineMiddlewareFactory<CheckRoleOptions>((options) => {
const allowedRoles = options?.roles ?? [];
return async (req, res, next) => {
const user = (req as any).user;
if (!user) {
req.app.throw(401, 'Authentication required');
}
if (allowedRoles.length > 0 && !allowedRoles.includes(user.role)) {
req.app.throw(403, 'Insufficient permissions');
}
await next();
};
});
为什么需要显式标记?
defineMiddleware 和 defineMiddlewareFactory 通过 Symbol 标记让中间件类型显式化。middleware-loader 通过 isMiddleware() / isMiddlewareFactory() 检测标记,零歧义地区分普通中间件和工厂中间件。
如果不标记,框架无法区分"一个函数到底是中间件本身,还是返回中间件的工厂函数"。
注册与使用
中间件的使用分为两步:配置白名单 → 路由引用。
Step 1: 在配置中声明白名单
所有路由级中间件必须先在 config/default.ts 的 middlewares 数组中声明:
// src/config/default.ts
export default {
port: 3000,
middlewares: [
// 普通中间件 — 字符串声明
'auth',
// 工厂中间件 — 对象声明(附带默认参数)
{ name: 'check-role', options: { roles: ['user'] } },
// 工厂中间件 — 无默认参数
'rate-limit-api',
],
};
白名单机制的好处:
- 安全性:防止路由随意引用未审核的中间件
- 显式依赖:一眼看到项目使用了哪些中间件
- 参数默认值:工厂中间件的默认参数集中管理
Step 2: 在路由中引用
通过 options.middlewares 为路由指定中间件:
// src/routes/admin.ts
import { defineRoutes } from 'vextjs';
export default defineRoutes((app) => {
// 字符串引用 — 使用配置中的默认参数
app.get('/profile', {
middlewares: ['auth'],
}, async (req, res) => {
res.json((req as any).user);
});
// 对象引用 — 覆盖默认参数
app.delete('/users/:id', {
middlewares: [
'auth',
{ name: 'check-role', options: { roles: ['superadmin'] } },
],
}, async (req, res) => {
const { id } = req.valid('param');
await app.services.user.delete(id);
res.status(204).json(null);
});
});
参数优先级
当工厂中间件同时在配置和路由中指定了参数时,路由级参数覆盖配置级默认参数:
配置默认参数 (config/default.ts) → 路由覆盖参数 (options.middlewares)
{ roles: ['user'] } → { roles: ['superadmin'] }
中间件执行顺序
全局中间件
VextJS 内置了多个全局中间件,在所有路由之前自动执行。执行顺序:
请求进入
↓
1. requestId — 生成/透传请求唯一标识
2. cors — CORS 跨域处理
3. bodyParser — 请求体解析(JSON / URL-encoded)
4. rateLimit — 全局速率限制
5. accessLog — 访问日志记录
6. responseWrapper — 开启响应包装({ code, data, requestId })
↓
7. [路由级中间件] — 按 options.middlewares 声明顺序
↓
8. [validateMiddleware] — 参数校验(如果配置了 validate)
↓
9. [handler] — 路由处理函数
↓
errorHandler — 全局错误处理(捕获任何阶段抛出的异常)
全局中间件通过配置控制行为(如 cors、rateLimit),但不能被路由跳过——它们对所有路由生效。
路由级中间件
路由级中间件按 options.middlewares 数组中的声明顺序执行:
app.post('/sensitive-action', {
middlewares: ['auth', 'check-role', 'audit-log'],
// ↑ 1st ↑ 2nd ↑ 3rd
}, handler);
全局中间件(插件注册)
插件可以通过 app.use() 注册全局中间件,对所有路由生效。这些中间件在内置全局中间件之后、路由级中间件之前执行:
// src/plugins/security-headers.ts
import { definePlugin } from 'vextjs';
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');
});
},
});
注意
app.use() 只能在插件的 setup() 中调用。路由注册完成后再调用将抛出错误。
常见中间件示例
认证中间件
// src/middlewares/auth.ts
import { defineMiddleware } from 'vextjs';
export default defineMiddleware(async (req, res, next) => {
const header = req.headers['authorization'];
if (!header?.startsWith('Bearer ')) {
req.app.throw(401, 'Missing or invalid Authorization header');
}
const token = header.slice(7);
try {
// 验证 JWT token
const payload = await verifyToken(token);
(req as any).user = payload;
} catch (err) {
req.app.throw(401, 'Token expired or invalid');
}
await next();
});
async function verifyToken(token: string) {
// 实际实现中使用 jsonwebtoken 或 jose 等库
return { id: '1', email: 'user@example.com', role: 'user' };
}
角色检查中间件
// src/middlewares/check-role.ts
import { defineMiddlewareFactory } from 'vextjs';
interface RoleOptions {
roles: string[];
}
export default defineMiddlewareFactory<RoleOptions>((options) => {
return async (req, res, next) => {
const user = (req as any).user;
if (!user) {
req.app.throw(401, 'Not authenticated');
}
const allowed = options?.roles ?? [];
if (allowed.length > 0 && !allowed.includes(user.role)) {
req.app.logger.warn(
{ userId: user.id, role: user.role, required: allowed },
'Access denied: insufficient role',
);
req.app.throw(403, 'Access denied');
}
await next();
};
});
请求耗时记录
// src/middlewares/timing.ts
import { defineMiddleware } from 'vextjs';
export default defineMiddleware(async (req, res, next) => {
const start = performance.now();
await next();
const duration = (performance.now() - start).toFixed(2);
res.setHeader('X-Response-Time', `${duration}ms`);
req.app.logger.info({
method: req.method,
path: req.path,
status: res.statusCode,
duration: `${duration}ms`,
}, 'Request completed');
});
API Key 验证
// src/middlewares/api-key.ts
import { defineMiddlewareFactory } from 'vextjs';
interface ApiKeyOptions {
header?: string;
keys?: string[];
}
export default defineMiddlewareFactory<ApiKeyOptions>((options) => {
const headerName = options?.header ?? 'x-api-key';
const validKeys = new Set(options?.keys ?? []);
return async (req, res, next) => {
if (validKeys.size === 0) {
// 未配置 keys,跳过验证
await next();
return;
}
const apiKey = req.headers[headerName];
if (!apiKey || !validKeys.has(apiKey)) {
req.app.throw(401, 'Invalid API key');
}
await next();
};
});
缓存控制
// src/middlewares/cache-control.ts
import { defineMiddlewareFactory } from 'vextjs';
interface CacheOptions {
maxAge?: number; // 秒
directive?: string; // 'public' | 'private' | 'no-cache' | 'no-store'
}
export default defineMiddlewareFactory<CacheOptions>((options) => {
const maxAge = options?.maxAge ?? 0;
const directive = options?.directive ?? 'public';
const value = maxAge > 0 ? `${directive}, max-age=${maxAge}` : 'no-store';
return async (req, res, next) => {
await next();
res.setHeader('Cache-Control', value);
};
});
错误处理中间件
全局错误处理由框架内置的 error-handler 负责,它会捕获中间件链中抛出的所有异常:
HttpError(由 app.throw() 抛出)→ 转化为结构化 JSON 响应
VextValidationError(参数校验失败)→ 422 响应 + errors 数组
- 其他异常 → 500 Internal Server Error
你不需要手动编写错误处理中间件。如果需要自定义错误处理逻辑(如上报到 Sentry),推荐在插件中使用 app.use() 注册一个 try-catch 中间件:
// src/plugins/sentry.ts
import { definePlugin } from 'vextjs';
export default definePlugin({
name: 'sentry',
setup(app) {
app.use(async (req, res, next) => {
try {
await next();
} catch (err) {
// 上报错误到 Sentry
// Sentry.captureException(err);
app.logger.error({ err }, 'Captured by Sentry plugin');
// 重新抛出,让框架的 error-handler 处理响应
throw err;
}
});
},
});
中间件中的 req.app
路由级中间件没有 defineRoutes 的闭包 app,因此通过 req.app 访问框架能力:
export default defineMiddleware(async (req, res, next) => {
// 通过 req.app 访问各种框架能力
req.app.logger.info('Middleware executing'); // 日志
req.app.throw(403, 'Forbidden'); // 抛出错误
const config = req.app.config; // 读取配置
const userSvc = req.app.services.user; // 访问服务
await next();
});
环境级中间件配置覆盖
可以在环境配置文件中覆盖中间件的默认参数:
// src/config/default.ts
export default {
middlewares: [
'auth',
{ name: 'check-role', options: { roles: ['user'] } },
],
};
// src/config/development.ts — 开发环境关闭某些中间件
export default {
middlewares: [
{ name: 'check-role', options: { roles: [] } }, // 开发环境不检查角色
],
};
配置的 middlewares 数组使用智能 patch 策略:按 name 匹配并合并,不会简单地替换整个数组。
内置中间件
VextJS 内置以下全局中间件,通过配置项控制行为:
详见 配置 章节了解各项配置选项。
TypeScript 类型扩展
如果中间件在 req 上挂载了自定义属性(如 req.user),推荐通过 declare module 扩展类型:
// src/types/extensions.d.ts
declare module 'vextjs' {
interface VextRequest {
user?: {
id: string;
email: string;
role: string;
};
}
}
扩展后,所有路由和中间件中访问 req.user 都会获得类型提示,无需 as any 断言。
最佳实践
1. 保持中间件职责单一
每个中间件只做一件事。认证和授权应分为两个中间件:
// ✅ 正确 — 职责单一
middlewares: ['auth', 'check-role']
// ❌ 避免 — 一个中间件做太多事
middlewares: ['auth-and-role-check']
2. 始终 await next()
如果中间件需要执行后置逻辑或让请求继续传递,必须 await next():
// ✅ 正确
export default defineMiddleware(async (req, res, next) => {
console.log('before');
await next(); // 等待后续中间件和 handler 完成
console.log('after');
});
// ❌ 错误 — 忘记 await,后置逻辑会在 handler 完成前执行
export default defineMiddleware(async (req, res, next) => {
console.log('before');
next(); // 没有 await!
console.log('after — 这会在 handler 之前执行');
});
3. 短路响应
某些中间件可能需要直接响应而不调用 next()(如认证失败)。在这种情况下直接返回即可,不需要调用 next():
export default defineMiddleware(async (req, res, next) => {
if (!isAllowed(req)) {
// 直接抛出错误,不调用 next() — 请求在此终止
req.app.throw(403, 'Access denied');
}
await next();
});
由于 app.throw() 的返回类型是 never,它会自动终止执行流程。
4. 在配置中管理,而非硬编码
避免在中间件内部硬编码配置值。使用工厂模式接收参数,在配置文件中统一管理:
// ✅ 正确 — 参数由配置管理
export default defineMiddlewareFactory<{ maxAge: number }>((options) => {
const maxAge = options?.maxAge ?? 3600;
return async (req, res, next) => {
await next();
res.setHeader('Cache-Control', `public, max-age=${maxAge}`);
};
});
// ❌ 避免 — 硬编码
export default defineMiddleware(async (req, res, next) => {
await next();
res.setHeader('Cache-Control', 'public, max-age=3600'); // 无法按环境变更
});
下一步
- 学习 插件 如何通过
app.use() 注册全局中间件
- 了解 参数校验 中间件的自动生成
- 查看 配置 中内置中间件的完整选项
- 探索 测试 如何测试中间件逻辑