中间件

VextJS 的中间件采用 洋葱模型(Onion Model),支持请求前处理和响应后处理。框架提供 defineMiddlewaredefineMiddlewareFactory 两种定义方式,通过约定式目录自动扫描加载。

洋葱模型

中间件通过 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;
参数说明
req框架统一的请求对象(与 Adapter 解耦)
res框架统一的响应对象
next调用下一个中间件;必须 await,否则后置逻辑无法正确执行

定义中间件

中间件文件放在 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();
  };
});
为什么需要显式标记?

defineMiddlewaredefineMiddlewareFactory 通过 Symbol 标记让中间件类型显式化。middleware-loader 通过 isMiddleware() / isMiddlewareFactory() 检测标记,零歧义地区分普通中间件和工厂中间件。

如果不标记,框架无法区分"一个函数到底是中间件本身,还是返回中间件的工厂函数"。

注册与使用

中间件的使用分为两步:配置白名单路由引用

Step 1: 在配置中声明白名单

所有路由级中间件必须先在 config/default.tsmiddlewares 数组中声明:

// 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      — 全局错误处理(捕获任何阶段抛出的异常)

全局中间件通过配置控制行为(如 corsrateLimit),但不能被路由跳过——它们对所有路由生效。

路由级中间件

路由级中间件按 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 内置以下全局中间件,通过配置项控制行为:

中间件配置项说明
requestIdconfig.requestId生成/透传请求唯一标识
corsconfig.corsCORS 跨域处理
bodyParserconfig.bodyParser请求体解析(JSON / URL-encoded)
rateLimitconfig.rateLimit全局速率限制
accessLogconfig.accessLog访问日志(method / path / status / duration)
responseWrapperconfig.response响应出口包装 { code, data, requestId }
errorHandler全局错误处理(不可配置,始终启用)

详见 配置 章节了解各项配置选项。

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