代码组织与架构设计原则
代码组织与架构设计原则
Express作为Node.js最流行的Web框架之一,其灵活性和轻量级特性使得开发者可以快速构建应用。但随着项目规模扩大,合理的代码组织和架构设计成为维护性、扩展性的关键因素。良好的架构能显著降低后期维护成本,提升团队协作效率。
分层架构原则
Express应用通常采用分层架构模式,将不同职责的代码分离到独立层次。典型的三层结构包括:
- 路由层:处理HTTP请求和响应
- 服务层:包含业务逻辑
- 数据访问层:负责与数据库交互
// 路由层示例
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 {
constructor(userRepository) {
this.userRepository = userRepository;
}
async getUserById(id) {
return this.userRepository.findById(id);
}
}
// 数据访问层示例
class UserRepository {
async findById(id) {
return db.query('SELECT * FROM users WHERE id = ?', [id]);
}
}
这种分层结构使得各层职责单一,便于单元测试和代码复用。修改数据访问实现时不会影响业务逻辑,调整业务规则也不会波及路由处理。
单一职责原则
每个模块、类或函数应该只有一个引起变化的原因。在Express中,这意味着:
- 路由处理函数只负责接收请求和返回响应
- 业务逻辑集中在服务类中
- 数据操作封装在专门的仓库类
违反单一职责的典型表现是"胖控制器":
// 不良实践:路由处理函数包含过多职责
router.post('/products', async (req, res) => {
// 验证输入
if (!req.body.name || !req.body.price) {
return res.status(400).json({ error: 'Missing fields' });
}
// 业务逻辑
const discount = req.body.price > 100 ? 0.9 : 1;
const finalPrice = req.body.price * discount;
// 数据操作
const product = await db.query(
'INSERT INTO products (name, price) VALUES (?, ?)',
[req.body.name, finalPrice]
);
// 发送通知
await emailService.sendNewProductNotification(product);
res.status(201).json(product);
});
重构后符合单一职责原则的代码:
// 路由层
router.post('/products', productController.createProduct);
// 控制器
class ProductController {
constructor(productService) {
this.productService = productService;
}
async createProduct(req, res) {
try {
const product = await this.productService.createProduct(req.body);
res.status(201).json(product);
} catch (error) {
res.status(400).json({ error: error.message });
}
}
}
// 服务层
class ProductService {
constructor(productRepository, notificationService) {
this.productRepository = productRepository;
this.notificationService = notificationService;
}
async createProduct(productData) {
this.validateProductData(productData);
const finalPrice = this.applyDiscount(productData.price);
const product = await this.productRepository.create({
...productData,
price: finalPrice
});
await this.notificationService.notifyNewProduct(product);
return product;
}
}
依赖注入原则
依赖注入(DI)是实现松耦合架构的重要手段。Express中可以通过构造函数注入依赖:
// 依赖注入示例
const express = require('express');
const { UserService } = require('./services/user.service');
const { UserRepository } = require('./repositories/user.repository');
const { UserController } = require('./controllers/user.controller');
const app = express();
const userRepository = new UserRepository(db);
const userService = new UserService(userRepository);
const userController = new UserController(userService);
app.get('/users/:id', (req, res) => userController.getUser(req, res));
这种方式便于:
- 单元测试时可以轻松注入mock对象
- 替换实现时不影响其他组件
- 明确组件间的依赖关系
模块化组织
Express应用的文件结构应该反映其架构设计。常见的组织方式:
/src
/config # 配置文件
/controllers # 路由控制器
/services # 业务服务
/repositories # 数据访问
/models # 数据模型
/middlewares # 中间件
/routes # 路由定义
/utils # 工具函数
app.js # 应用入口
每个模块应该:
- 有明确的单一职责
- 通过index.js暴露公共接口
- 保持内部实现私有
// 模块示例:/services/user.service.js
class UserService {
// 实现细节
}
module.exports = { UserService };
// /services/index.js
const { UserService } = require('./user.service');
const { ProductService } = require('./product.service');
module.exports = {
UserService,
ProductService
};
中间件的合理使用
Express中间件是强大的功能,但需要谨慎组织:
- 全局中间件:应用于所有路由的中间件(如日志、body解析)
- 路由中间件:特定路由组的中间件(如认证)
- 错误处理中间件:专门处理错误的中间件
// 全局中间件
app.use(express.json());
app.use(requestLogger);
// 路由组中间件
const authRouter = express.Router();
authRouter.use(authenticate);
authRouter.get('/profile', profileController.getProfile);
// 错误处理中间件
app.use((err, req, res, next) => {
logger.error(err.stack);
res.status(500).send('Something broke!');
});
避免在中间件中嵌入业务逻辑,中间件应该只处理横切关注点(cross-cutting concerns)如:
- 认证授权
- 请求日志
- 数据验证
- 响应格式化
配置管理
将配置与代码分离是重要的架构原则。Express应用通常需要管理:
- 数据库连接配置
- 第三方API密钥
- 环境特定变量(开发/测试/生产)
推荐使用dotenv管理环境变量:
// .env 文件
DB_HOST=localhost
DB_PORT=5432
DB_USER=myuser
DB_PASS=mypassword
// config/database.js
require('dotenv').config();
module.exports = {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
user: process.env.DB_USER,
password: process.env.DB_PASS
};
对于更复杂的配置,可以使用配置对象:
// config/index.js
const dev = {
app: {
port: 3000
},
db: {
host: 'localhost',
port: 27017,
name: 'dev_db'
}
};
const prod = {
app: {
port: 80
},
db: {
host: 'cluster.mongodb.net',
port: 27017,
name: 'prod_db'
}
};
const config = {
dev,
prod
};
module.exports = config[process.env.NODE_ENV || 'dev'];
错误处理策略
统一的错误处理机制对维护性至关重要。Express中推荐的做法:
- 使用自定义错误类区分错误类型
- 中间件集中处理错误
- 标准化错误响应格式
// errors/app-error.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} not found`, 404);
}
}
module.exports = {
AppError,
NotFoundError
};
// 控制器中使用
const { NotFoundError } = require('../errors/app-error');
async function getUser(req, res, next) {
try {
const user = await userService.getUser(req.params.id);
if (!user) {
throw new NotFoundError('User');
}
res.json(user);
} catch (error) {
next(error);
}
}
// 错误处理中间件
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
error: {
message: err.message,
type: err.name,
details: err.details
}
});
});
测试友好设计
良好的架构应该便于测试。在Express中这意味着:
- 避免直接require模块,使用依赖注入
- 分离副作用代码(如数据库访问)
- 保持函数纯净(同样输入总是同样输出)
// 可测试的服务示例
class OrderService {
constructor(inventoryService, paymentGateway) {
this.inventoryService = inventoryService;
this.paymentGateway = paymentGateway;
}
async placeOrder(order) {
await this.inventoryService.reserveItems(order.items);
const receipt = await this.paymentGateway.charge(order.total);
return { ...order, receipt };
}
}
// 测试示例
describe('OrderService', () => {
it('should place order successfully', async () => {
const mockInventory = { reserveItems: jest.fn() };
const mockGateway = { charge: jest.fn().mockResolvedValue('receipt123') };
const service = new OrderService(mockInventory, mockGateway);
const order = { items: ['item1'], total: 100 };
const result = await service.placeOrder(order);
expect(mockInventory.reserveItems).toHaveBeenCalledWith(['item1']);
expect(mockGateway.charge).toHaveBeenCalledWith(100);
expect(result.receipt).toBe('receipt123');
});
});
性能考量
架构设计需要考虑性能因素:
- 中间件顺序:将常用中间件放在前面,过滤无效请求
- 路由组织:将高频路由放在前面
- 缓存策略:实现适当的缓存层
// 优化中间件顺序
app.use(helmet()); // 安全相关优先
app.use(compression()); // 压缩响应
app.use(rateLimiter); // 限流
app.use(authMiddleware); // 认证
app.use('/api', apiRouter); // 业务路由
对于数据密集型操作,考虑添加缓存层:
// 缓存装饰器示例
function cache(ttl) {
return function(target, name, descriptor) {
const original = descriptor.value;
descriptor.value = async function(...args) {
const cacheKey = `${name}-${JSON.stringify(args)}`;
const cached = await cacheClient.get(cacheKey);
if (cached) return JSON.parse(cached);
const result = await original.apply(this, args);
await cacheClient.set(cacheKey, JSON.stringify(result), 'EX', ttl);
return result;
};
return descriptor;
};
}
// 使用缓存装饰器
class ProductService {
@cache(60) // 缓存60秒
async getPopularProducts() {
return this.productRepository.findPopular();
}
}
扩展性设计
随着业务增长,架构需要支持水平扩展:
- 无状态设计:避免在内存中存储会话状态
- 队列集成:耗时操作异步处理
- 微服务准备:通过模块边界为未来拆分做准备
// 使用Redis存储会话
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'your-secret',
resave: false,
saveUninitialized: false
}));
// 异步任务处理示例
router.post('/reports', async (req, res) => {
const reportId = uuidv4();
await queue.add('generate-report', {
reportId,
userId: req.user.id,
params: req.body
});
res.json({ reportId, status: 'queued' });
});
// Worker处理任务
queue.process('generate-report', async (job) => {
const { reportId, userId, params } = job.data;
const report = await reportService.generateReport(userId, params);
await storage.save(reportId, report);
await emailService.sendReportReady(userId, reportId);
});
文档与一致性
良好的代码组织需要配套的文档和规范:
- API文档:使用Swagger或OpenAPI
- 架构决策记录:记录重大设计决策
- 代码风格指南:保持代码一致性
// Swagger文档示例
/**
* @swagger
* /users/{id}:
* get:
* summary: Get user by ID
* tags: [Users]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: The user
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/User'
*/
router.get('/users/:id', userController.getUser);
架构决策记录示例(在docs/adr目录下):
001-use-express-framework.md
002-layered-architecture.md
003-postgresql-as-primary-db.md
现代化改进方向
随着技术演进,可以考虑:
- TypeScript集成:增强类型安全
- GraphQL混合:部分API采用GraphQL
- Serverless适配:部署到函数计算
// TypeScript控制器示例
import { Request, Response } from 'express';
import { UserService } from '../services/user.service';
export class UserController {
constructor(private userService: UserService) {}
async getUser(req: Request, res: Response): Promise<void> {
try {
const user = await this.userService.getUser(req.params.id);
if (!user) {
res.status(404).json({ error: 'User not found' });
return;
}
res.json(user);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
}
持续演进与重构
架构不是一成不变的,需要定期评估和调整:
- 监控痛点:识别经常修改的模块
- 技术债务追踪:记录需要改进的地方
- 渐进式重构:小步迭代而非大规模重写
重构示例:将内联中间件提取为独立模块
// 重构前
app.use((req, res, next) => {
if (!req.headers['x-api-key']) {
return res.status(401).send('API key required');
}
if (req.headers['x-api-key'] !== process.env.API_KEY) {
return res.status(403).send('Invalid API key');
}
next();
});
// 重构后:/middlewares/api-auth.js
function apiAuth(req, res, next) {
if (!req.headers['x-api-key']) {
return res.status(401).send('API key required');
}
if (req.headers['x-api-key'] !== process.env.API_KEY) {
return res.status(403).send('Invalid API key');
}
next();
}
module.exports = apiAuth;
// 使用重构后的中间件
const apiAuth = require('./middlewares/api-auth');
app.use(apiAuth);
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
下一篇:性能瓶颈分析与优化