您现在的位置是:网站首页 > 代码组织与架构设计原则文章详情
代码组织与架构设计原则
陈川
【
Node.js
】
46417人已围观
10438字
代码组织与架构设计原则
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 {
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中间件是强大的功能,但需要谨慎组织:
- 全局中间件:应用于所有路由(如日志、CORS)
- 路由级中间件:特定路由组使用(如认证检查)
- 错误处理中间件:集中处理错误
// 中间件组织示例
// ----------------------------
// 全局中间件
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]);
性能考量
架构设计应考虑性能因素:
- 中间件顺序:频繁使用的中间件放在前面
- 路由优化:具体路由放在通用路由前面
- 缓存策略:实现接口缓存
// 低效的路由顺序
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'
});
});
上一篇: Express生态系统的挑战与机遇
下一篇: 性能瓶颈分析与优化