您现在的位置是:网站首页 > 代码组织与架构设计原则文章详情

代码组织与架构设计原则

代码组织与架构设计原则

Express作为Node.js最流行的Web框架之一,其灵活性和轻量级特性使得开发者可以快速构建应用。但随着项目规模扩大,合理的代码组织和架构设计成为维护性、扩展性的关键因素。良好的架构能显著降低迭代成本,而混乱的代码结构则会导致"面条式代码"问题。

分层架构模式

Express应用通常采用分层架构,将不同职责的代码分离到特定层级。典型的三层结构包括:

  1. 路由层:处理HTTP请求的路由分发
  2. 服务层:包含核心业务逻辑
  3. 数据访问层:负责与数据库交互
// 路由层示例
router.get('/users/:id', async (req, res) => {
  try {
    const user = await userService.getUserById(req.params.id);
    res.json(user);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// 服务层示例
class UserService {
  async getUserById(id) {
    const user = await userRepository.findById(id);
    if (!user) throw new Error('User not found');
    return user;
  }
}

// 数据访问层示例
class UserRepository {
  async findById(id) {
    return db.query('SELECT * FROM users WHERE id = ?', [id]);
  }
}

单一职责原则

每个模块/类/函数应该只负责一个明确的功能点。违反这一原则的典型表现是"上帝对象",即一个类承担过多职责。例如,用户控制器不应同时处理认证、数据验证和业务逻辑:

// 违反单一职责的反例
class UserController {
  async createUser(req, res) {
    // 验证输入
    if (!req.body.email) {
      return res.status(400).json({ error: 'Email is required' });
    }
    
    // 业务逻辑
    const hashedPassword = bcrypt.hash(req.body.password, 10);
    
    // 数据库操作
    const user = await db.query(
      'INSERT INTO users (email, password) VALUES (?, ?)',
      [req.body.email, hashedPassword]
    );
    
    // 发送邮件
    await mailer.sendWelcomeEmail(user.email);
    
    return res.json(user);
  }
}

// 改进后的结构
class UserValidator { /* 验证逻辑 */ }
class UserService { /* 业务逻辑 */ }
class UserRepository { /* 数据库操作 */ }
class EmailService { /* 邮件发送 */ }

依赖注入与控制反转

Express应用应避免硬编码依赖关系,而采用依赖注入(DI)模式。这提高了代码的可测试性和模块化程度:

// 传统方式(强耦合)
const userService = require('./services/userService');

router.get('/users', (req, res) => {
  userService.getAllUsers().then(users => res.json(users));
});

// 依赖注入方式
module.exports = (userService) => {
  const router = express.Router();
  
  router.get('/users', (req, res) => {
    userService.getAllUsers().then(users => res.json(users));
  });
  
  return router;
};

// 应用启动时注入依赖
const userService = require('./services/userService');
const userRoutes = require('./routes/users')(userService);
app.use('/api', userRoutes);

中间件的合理使用

Express中间件是强大的功能,但需要谨慎组织:

  1. 全局中间件:应用于所有路由(如日志、CORS)
  2. 路由级中间件:特定路由组使用(如认证检查)
  3. 错误处理中间件:集中处理错误
// 中间件组织示例
// ----------------------------
// 全局中间件
app.use(express.json());
app.use(loggerMiddleware);

// 路由组中间件
const authRouter = express.Router();
authRouter.use(jwtAuthMiddleware);
authRouter.get('/profile', profileController);

// 错误处理中间件(放在最后)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

配置管理

应用配置应该与代码分离,并考虑不同环境(开发、测试、生产)的差异:

// config/index.js
const env = process.env.NODE_ENV || 'development';
const config = {
  development: {
    db: { url: 'localhost:5432/dev' },
    port: 3000
  },
  production: {
    db: { url: process.env.DATABASE_URL },
    port: process.env.PORT
  }
};

module.exports = config[env];

// app.js
const config = require('./config');
const db = connect(config.db);
app.listen(config.port);

模块化路由设计

大型应用的路由应该按功能模块拆分,避免单一路由文件膨胀:

routes/
├── index.js        # 聚合所有路由
├── users/
│   ├── index.js    # 用户相关路由
│   └── profile.js  # 个人资料子路由
└── products/       # 产品相关路由
// routes/users/index.js
const router = require('express').Router();
const profileRouter = require('./profile');

router.use('/profile', profileRouter);
router.get('/', userController.getAll);

module.exports = router;

// app.js
const userRoutes = require('./routes/users');
app.use('/users', userRoutes);

错误处理策略

统一的错误处理机制能避免重复代码并提高可维护性:

// 自定义错误类
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor);
  }
}

// 控制器中使用
async function createUser(req, res, next) {
  try {
    const existingUser = await User.findOne({ email: req.body.email });
    if (existingUser) {
      throw new AppError('Email already exists', 400);
    }
    // ...创建用户逻辑
  } catch (err) {
    next(err);
  }
}

// 全局错误处理器
app.use((err, req, res, next) => {
  err.statusCode = err.statusCode || 500;
  res.status(err.statusCode).json({
    status: err.status,
    message: err.message
  });
});

测试友好设计

良好的架构应该便于单元测试和集成测试:

// 不易测试的代码
const db = require('../../db');
exports.getUser = async (id) => {
  return db.query('SELECT * FROM users WHERE id = ?', [id]);
};

// 可测试的代码
class UserRepository {
  constructor(database) {
    this.db = database;
  }
  
  async getUser(id) {
    return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
  }
}

// 测试示例
const mockDb = {
  query: jest.fn().mockResolvedValue({ id: 1, name: 'Test' })
};
const repo = new UserRepository(mockDb);
await repo.getUser(1);
expect(mockDb.query).toHaveBeenCalledWith('SELECT * FROM users WHERE id = ?', [1]);

性能考量

架构设计应考虑性能因素:

  1. 中间件顺序:频繁使用的中间件放在前面
  2. 路由优化:具体路由放在通用路由前面
  3. 缓存策略:实现接口缓存
// 低效的路由顺序
app.get('/users/:id', getUserById); 
app.get('/users/me', authMiddleware, getCurrentUser);

// 优化后的顺序(更具体的路由在前)
app.get('/users/me', authMiddleware, getCurrentUser);
app.get('/users/:id', getUserById);

// 缓存中间件示例
function cacheMiddleware(duration) {
  return (req, res, next) => {
    const key = req.originalUrl;
    const cached = cache.get(key);
    
    if (cached) {
      return res.json(cached);
    }
    
    const originalSend = res.json;
    res.json = (body) => {
      cache.set(key, body, duration);
      originalSend.call(res, body);
    };
    
    next();
  };
}

router.get('/products', cacheMiddleware(60), productController.getAll);

现代JavaScript特性应用

合理使用ES6+特性提升代码质量:

// 使用async/await替代回调
router.get('/posts', async (req, res, next) => {
  try {
    const posts = await postService.getLatestPosts();
    res.json(posts);
  } catch (err) {
    next(err);
  }
});

// 使用解构和默认参数
function createUser({
  name,
  email,
  role = 'user',
  status = 'active'
} = {}) {
  // ...
}

// 使用类字段语法
class Logger {
  logLevel = process.env.LOG_LEVEL || 'info';
  
  log(message) {
    console.log(`[${this.logLevel}] ${message}`);
  }
}

类型检查与验证

即使不使用TypeScript,也应添加数据验证:

// 使用Joi进行验证
const Joi = require('joi');

const userSchema = Joi.object({
  name: Joi.string().min(3).required(),
  email: Joi.string().email().required(),
  age: Joi.number().min(18)
});

router.post('/users', (req, res, next) => {
  const { error } = userSchema.validate(req.body);
  if (error) {
    return res.status(400).json({ error: error.details[0].message });
  }
  // 处理有效数据
});

// 使用TypeScript接口
interface User {
  id: number;
  name: string;
  email: string;
}

class UserRepository {
  async findById(id: number): Promise<User> {
    // ...
  }
}

日志记录策略

完善的日志系统对生产环境至关重要:

// 自定义日志中间件
function requestLogger(req, res, next) {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    logger.info({
      method: req.method,
      url: req.originalUrl,
      status: res.statusCode,
      duration: `${duration}ms`,
      ip: req.ip
    });
  });
  
  next();
}

// 错误日志
app.use((err, req, res, next) => {
  logger.error({
    message: err.message,
    stack: err.stack,
    request: {
      method: req.method,
      url: req.originalUrl,
      body: req.body
    }
  });
  next(err);
});

安全最佳实践

架构设计必须考虑安全性:

// 安全相关中间件
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');

app.use(helmet());
app.use(rateLimit({
  windowMs: 15 * 60 * 1000, // 15分钟
  max: 100 // 每个IP限制100次请求
}));

// 敏感数据过滤
class UserSerializer {
  static serialize(user) {
    return {
      id: user.id,
      name: user.name,
      email: user.email
      // 不返回password和token等字段
    };
  }
}

// 控制器中使用
router.get('/users/:id', (req, res) => {
  const user = await userService.getById(req.params.id);
  res.json(UserSerializer.serialize(user));
});

部署与扩展考虑

架构应支持水平扩展:

// 无状态设计
// 避免在内存中存储会话数据
const session = require('express-session');
const RedisStore = require('connect-redis')(session);

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false
}));

// 任务队列集成
const Queue = require('bull');
const emailQueue = new Queue('emails', {
  redis: {
    host: 'redis-server',
    port: 6379
  }
});

// 控制器中将耗时操作放入队列
router.post('/newsletter', (req, res) => {
  emailQueue.add('send-to-all', {
    subject: req.body.subject,
    content: req.body.content
  });
  res.json({ status: 'queued' });
});

代码风格一致性

统一代码风格提高可读性:

// 路由命名约定
// GET /users - 获取用户列表
// POST /users - 创建用户
// GET /users/:id - 获取单个用户
// PUT /users/:id - 更新用户
// DELETE /users/:id - 删除用户

// 文件命名约定
// controllers/userController.js
// services/userService.js
// models/userModel.js
// routes/userRoutes.js

// 使用ESLint配置
module.exports = {
  extends: ['airbnb-base'],
  rules: {
    'linebreak-style': ['error', 'unix'],
    'no-console': ['error', { allow: ['warn', 'error'] }]
  }
};

文档与注释规范

适当的文档和注释帮助团队协作:

/**
 * 用户服务类
 * 处理所有与用户相关的业务逻辑
 */
class UserService {
  /**
   * 根据ID获取用户
   * @param {number} id - 用户ID
   * @returns {Promise<User>} 用户对象
   * @throws {AppError} 当用户不存在时抛出404错误
   */
  async getUserById(id) {
    // 实现代码
  }
}

// 路由文档示例(可使用Swagger等工具)
/**
 * @swagger
 * /users/{id}:
 *   get:
 *     summary: 获取用户信息
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema:
 *           type: integer
 *     responses:
 *       200:
 *         description: 用户对象
 */
router.get('/users/:id', userController.getById);

持续集成与部署

架构应支持CI/CD流程:

// 测试脚本示例
// package.json
{
  "scripts": {
    "test": "jest --coverage",
    "lint": "eslint .",
    "start": "node app.js",
    "start:dev": "nodemon app.js"
  }
}

// Dockerfile示例
FROM node:14
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

// 健康检查端点
router.get('/health', (req, res) => {
  res.json({
    status: 'up',
    timestamp: new Date(),
    dbStatus: db.readyState === 1 ? 'connected' : 'disconnected'
  });
});

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步