中间件
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() 注册全局中间件
- 了解 参数校验 中间件的自动生成
- 查看 配置 中内置中间件的完整选项
- 探索 测试 如何测试中间件逻辑