人人都会AI编程

中间件层、工具层、常量层划分

更新时间:2026-07-10

在介绍了 Controller、Service、DAO 三层之后,一个结构清晰的后端项目还需要几类“横切”性质的代码来支撑整个应用。它们不直接对应某个业务功能,而是提供通用能力、辅助函数和全局常量,贯穿所有业务层。按照职责不同,我们通常把它们归入中间件层工具层常量层。下面逐一说明它们的定位、组织方式以及实际开发中的最佳实践。

中间件层:请求链路上的横切逻辑

中间件(Middleware)是 Node.js Web 框架中特有的概念,一个请求在到达最终的 Controller 之前或之后,可以经过一系列中间件的处理。这些中间件负责诸如身份认证、请求日志、参数校验、跨域处理、异常捕获等与具体业务逻辑无关但每条请求几乎都需要的能力。

在一个标准分层架构中,中间件层通常放在 src/middlewares 目录下,每个文件导出一个或多个中间件函数。以 Express 或 Koa 为例的结构如下:

src/
├── middlewares/
│   ├── auth.js          // 身份认证中间件
│   ├── logger.js        // 请求日志
│   ├── errorHandler.js  // 全局异常处理
│   ├── validator.js     // 参数校验
│   └── rateLimiter.js   // 接口限流

中间件的职责边界

  • 身份认证与鉴权:解析请求中的 Token 或 Session,将用户信息挂载到 req 上,失败时直接返回 401。它不应该涉及具体的权限判断,那是业务层(如 Guard 或 Service 中的权限校验)的职责。
  • 请求日志:记录方法、URL、状态码、耗时等,与业务无关,且应在请求进入和响应结束时完成。
  • 异常捕获:最外层的中间件负责捕获所有未处理的异常,统一返回友好的错误格式,并记录堆栈信息。
  • 参数校验与转换:根据定义的 Schema 验证 query、body、params,并将有效数据挂载到 req.validated 上,Controller 只操作已校验的数据。
  • 接口限流与防刷:基于 Redis 或内存记录访问频率,对超频请求直接拒绝。

中间件的实现示例(以 Express 风格为例,Koa 类似):

// src/middlewares/auth.js
module.exports = function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ code: 401, message: '未提供认证令牌' });
  }
  try {
    const user = jwt.verify(token, process.env.JWT_SECRET);
    req.user = user;   // 挂载当前用户信息
    next();
  } catch (err) {
    return res.status(401).json({ code: 401, message: '令牌无效或已过期' });
  }
};
// src/middlewares/errorHandler.js
module.exports = function errorHandler(err, req, res, next) {
  // 记录完整错误堆栈
  logger.error(`${req.method} ${req.url}`, err);
  // 区分已知的业务错误和未知内部错误
  const status = err.status || 500;
  const message = status === 500 ? '服务器内部错误' : err.message;
  res.status(status).json({ code: status, message });
};

中间件层的核心原则是与业务逻辑无关但被普遍需要。如果一个中间件开始查询数据库进行复杂判断,说明它的职责过重了,这类逻辑更适合放在 Service 或专门的 Guard/Policy 中。

工具层:无状态的纯函数与辅助模块

在项目中常常有一些被各个模块反复使用的功能,比如日期格式化、数据脱敏、ID 生成、文件路径处理等。它们不依赖任何请求上下文,也不进行数据库操作,只是接收输入、返回输出的纯逻辑。这就是工具层(或称为 utils/helpers)。

工具层通常放在 src/utilssrc/helpers 目录下,按功能模块拆分为多个文件:

src/
├── utils/
│   ├── formatDate.js     // 日期格式化
│   ├── maskPhone.js      // 手机号脱敏
│   ├── generateId.js     // 唯一ID生成
│   ├── pagination.js     // 分页数据格式化
│   └── crypto.js         // 加解密工具

工具层的设计原则

  • 纯函数优先:同一个输入永远得到同一个输出,不产生副作用,可以安全地在任何地方调用。
  • 不依赖全局状态或请求上下文:如果需要当前用户信息或数据库连接,那么它不应该在这里,应该放到 Service 中。
  • 命名清晰,粒度适中:一个工具文件通常只做一件事,函数命名明确表达意图,如 formatDate(date, formatStr)maskPhone(phone)

工具层示例

// src/utils/pagination.js
/**
 * 统一分页响应格式
 * @param {Array} list 当前页数据
 * @param {number} total 总条数
 * @param {number} page 当前页码
 * @param {number} pageSize 每页条数
 * @returns {Object} 标准分页结果
 */
function buildPaginationResult(list, total, page, pageSize) {
  return {
    list,
    total,
    page,
    pageSize,
    totalPages: Math.ceil(total / pageSize),
  };
}
// src/utils/maskPhone.js
function maskPhone(phone) {
  if (!phone || phone.length !== 11) return phone;
  return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}

工具层的函数不应该直接操作数据库,也不应该引入框架特定的对象(如 Express 的 req/res)。如果某个工具需要异步操作(如调用第三方 API),那它往往属于 Service 层的职责,需要单独封装。工具层可以包含一些纯计算的异步函数(如加密摘要运算),但要确保调用方清晰了解其行为。

常量层:集中管理的常量与枚举

项目里充斥着大量的魔法数字和字符串:接口返回的状态码、用户角色标识、订单状态、配置项键名等。把这些值散落在代码各处会导致维护困难(修改时到处寻找)、容易出错(拼写错误)且可读性差。常量层正是为了解决这一问题,将所有约定好的常量集中定义和管理。

常量层通常放在 src/constants 目录下,以模块文件组织:

src/
├── constants/
│   ├── index.js          // 统一导出
│   ├── httpStatus.js     // HTTP 状态码
│   ├── errorCode.js      // 自定义业务错误码
│   ├── userRole.js       // 用户角色枚举
│   └── orderStatus.js    // 订单状态常量

常量定义方式

  • 简单值常量:直接导出字符串或数字,如 JWT_EXPIRES_IN: '7d'
  • 对象映射:定义键值对映射关系,提供描述文本,例如订单状态的 PAIDSHIPPEDDELIVERED
  • 唯一枚举值:可以使用 Symbol() 或简单的字符串常量,确保不与其他值冲突。

常量层示例

// src/constants/orderStatus.js
const ORDER_STATUS = {
  PENDING: 0,      // 待支付
  PAID: 1,         // 已支付
  SHIPPED: 2,      // 已发货
  DELIVERED: 3,    // 已签收
  CANCELLED: 4,    // 已取消
  REFUNDING: 5,    // 退款中
};

const ORDER_STATUS_TEXT = {
  [ORDER_STATUS.PENDING]: '待支付',
  [ORDER_STATUS.PAID]: '已支付',
  // ...
};

module.exports = { ORDER_STATUS, ORDER_STATUS_TEXT };
// src/constants/errorCode.js
module.exports = {
  // 通用错误
  SUCCESS: 0,
  UNKNOWN_ERROR: 10000,
  // 用户相关 2xxxx
  USER_NOT_FOUND: 20001,
  PASSWORD_INVALID: 20002,
  TOKEN_EXPIRED: 20003,
  // 订单相关 3xxxx
  ORDER_NOT_FOUND: 30001,
  ORDER_STATUS_INVALID: 30002,
};

使用常量层的代码,从不直接写 if (order.status === 1),而是写成 if (order.status === ORDER_STATUS.PAID),可读性和可维护性都明显提升。同时,统一的错误码定义让前后端对接错误提示时更清晰。

三层协作的实际场景

当这三层联合运行时,请求处理流程会变得井井有条:

  1. 客户端请求 /api/orders
  2. 中间件层依次执行:logger 记录请求开始时间;auth 验证 JWT 并将 req.user 挂载;validator 根据规则校验查询参数并挂载 req.validated
  3. Controller 层拿到已验证的用户和参数,调用 OrderService.list(req.user.id, req.validated.page, req.validated.pageSize)
  4. Service 层处理业务逻辑:检查用户权限(可能使用常量层的 USER_ROLE.ADMIN 来判断)、调用 OrderRepository.findByUserId() 获取数据,使用 buildPaginationResult 格式化分页结果,将订单状态数字转换为对应的中文文本(使用常量层的 ORDER_STATUS_TEXT)。
  5. 如果过程中抛出业务错误,最外层的 errorHandler 中间件捕获,根据错误类型读取常量层的 errorCode,返回统一格式的错误响应。
  6. logger 中间件在响应结束时自动记录状态码和耗时。

在这一过程中,中间件层、工具层、常量层虽然没有直接实现业务逻辑,却大幅降低了各个业务模块的耦合度,让代码更加模块化、可测试、可维护。

反模式与避坑提醒

在设计这三层时,也有一些常见的坑需要避开:

  • 中间件中的“万能处理”:有人会把一段复杂的业务判断(如根据用户角色和订单状态决定是否退款)写进中间件,这会导致后续 Controller 变得莫名其妙地脆弱。中间件只应做通用过滤和预处理,业务决策应留在 Service。
  • 工具层依赖上下文:如果将当前请求的用户 ID 传入工具函数,并要求工具函数去查数据库获取用户信息,那它就违反了工具层的无状态原则,会让测试变得困难。这类逻辑应放在 Service。
  • 常量层过度细化:有些开发者把几乎所有字符串都抽成常量,包括“用户名为空”这种仅使用一次的错误消息。这样反而增加了查找和阅读成本。只有多处使用有业务含义需要统一管理的值才应该进入常量层。
  • 常量命名冲突:多个常量文件中使用相似的名称(如 STATUSTYPE)时,统一导出容易导致命名模糊。可以通过文件名区分或使用命名空间导出,例如 orderStatus.STATUS.PAID

正确划分和设计中间件层、工具层、常量层,会让项目的架构更加健壮,团队协作时也更容易理解每个模块的职责所在。配合之前的三层架构,后端项目就形成了一套从接口层到数据层、从业务逻辑到横切关注点的完整分层体系,为持续迭代打下坚实基础。