Appearance
Node.js 最佳实践
本章总结了Node.js开发中的最佳实践,涵盖代码组织、错误处理、性能优化、安全性和部署等方面。
项目结构
标准项目结构
my-node-app/
├── src/
│ ├── controllers/ # 控制器层
│ ├── models/ # 数据模型
│ ├── routes/ # 路由定义
│ ├── services/ # 业务逻辑层
│ ├── middleware/ # 中间件
│ ├── utils/ # 工具函数
│ ├── config/ # 配置文件
│ └── app.js # 应用入口
├── tests/ # 测试文件
│ ├── unit/ # 单元测试
│ ├── integration/ # 集成测试
│ └── e2e/ # 端到端测试
├── docs/ # 文档
├── scripts/ # 脚本文件
├── logs/ # 日志文件
├── uploads/ # 上传文件
├── .env # 环境变量
├── .gitignore
├── package.json
├── README.md
└── Dockerfile
模块组织最佳实践
javascript
// src/config/database.js
const config = {
development: {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME || 'myapp_dev',
username: process.env.DB_USER || 'user',
password: process.env.DB_PASSWORD || 'password'
},
production: {
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: true
}
};
module.exports = config[process.env.NODE_ENV || 'development'];
javascript
// src/models/User.js
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');
const User = sequelize.define('User', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notEmpty: true,
len: [1, 50]
}
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
isEmail: true
}
}
}, {
tableName: 'users',
timestamps: true
});
module.exports = User;
异步编程最佳实践
使用async/await
javascript
// 好的做法 - 使用async/await
class UserService {
async createUser(userData) {
try {
// 验证数据
this.validateUserData(userData);
// 检查用户是否已存在
const existingUser = await User.findOne({
where: { email: userData.email }
});
if (existingUser) {
throw new Error('用户已存在');
}
// 创建用户
const user = await User.create(userData);
// 发送欢迎邮件(非阻塞)
this.sendWelcomeEmail(user.email).catch(console.error);
return user;
} catch (error) {
// 记录错误
console.error('创建用户失败:', error);
throw error;
}
}
async sendWelcomeEmail(email) {
// 异步发送邮件
const mailOptions = {
to: email,
subject: '欢迎注册',
text: '欢迎加入我们!'
};
return await emailService.send(mailOptions);
}
validateUserData(userData) {
if (!userData.name || !userData.email) {
throw new Error('姓名和邮箱是必需的');
}
if (!this.isValidEmail(userData.email)) {
throw new Error('无效的邮箱格式');
}
}
isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}
错误处理最佳实践
javascript
// 自定义错误类
class AppError extends Error {
constructor(message, statusCode, isOperational = true) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = isOperational;
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
this.type = 'VALIDATION_ERROR';
}
}
class NotFoundError extends AppError {
constructor(message = '资源未找到') {
super(message, 404);
this.type = 'NOT_FOUND_ERROR';
}
}
// 在Express中间件中使用
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// 使用示例
app.post('/users', asyncHandler(async (req, res) => {
const user = await userService.createUser(req.body);
res.status(201).json({ user });
}));
// 全局错误处理中间件
app.use((err, req, res, next) => {
if (err instanceof AppError) {
res.status(err.statusCode).json({
status: err.status,
message: err.message
});
} else {
// 编程错误
console.error('未处理的错误:', err);
res.status(500).json({
status: 'error',
message: '服务器内部错误'
});
}
});
性能优化
数据库查询优化
javascript
// 避免N+1查询问题
class PostService {
// 不好的做法 - 会产生N+1查询
async getPostsWithAuthorsBad() {
const posts = await Post.findAll();
for (const post of posts) {
post.author = await User.findByPk(post.authorId); // N次查询
}
return posts;
}
// 好的做法 - 使用关联查询
async getPostsWithAuthorsGood() {
return await Post.findAll({
include: [{
model: User,
as: 'author',
attributes: ['id', 'name', 'email'] // 只选择需要的字段
}]
});
}
// 使用分页避免大数据集查询
async getPostsWithPagination(page = 1, limit = 10) {
const offset = (page - 1) * limit;
const { count, rows } = await Post.findAndCountAll({
include: [{
model: User,
as: 'author',
attributes: ['id', 'name']
}],
limit: parseInt(limit),
offset: parseInt(offset),
order: [['createdAt', 'DESC']]
});
return {
posts: rows,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total: count,
pages: Math.ceil(count / limit)
}
};
}
}
缓存策略
javascript
// 多层缓存实现
class CacheService {
constructor() {
this.memoryCache = new Map();
this.ttlMap = new Map(); // 存储过期时间
}
async get(key) {
// 首先检查内存缓存
if (this.memoryCache.has(key)) {
const ttl = this.ttlMap.get(key);
if (Date.now() < ttl) {
return this.memoryCache.get(key);
} else {
// 缓存过期,删除
this.memoryCache.delete(key);
this.ttlMap.delete(key);
}
}
// 检查Redis缓存
if (global.redisClient) {
try {
const cached = await global.redisClient.get(key);
if (cached) {
const parsed = JSON.parse(cached);
// 同步到内存缓存
this.memoryCache.set(key, parsed);
return parsed;
}
} catch (error) {
console.error('Redis缓存错误:', error);
}
}
return null;
}
async set(key, value, ttlSeconds = 300) {
// 设置内存缓存
this.memoryCache.set(key, value);
this.ttlMap.set(key, Date.now() + (ttlSeconds * 1000));
// 设置Redis缓存
if (global.redisClient) {
try {
await global.redisClient.setEx(key, ttlSeconds, JSON.stringify(value));
} catch (error) {
console.error('Redis缓存错误:', error);
}
}
}
async delete(key) {
this.memoryCache.delete(key);
this.ttlMap.delete(key);
if (global.redisClient) {
await global.redisClient.del(key);
}
}
}
// 缓存装饰器
function cache(ttlSeconds = 300) {
return function(target, propertyName, descriptor) {
const method = descriptor.value;
const cacheKeyPrefix = `${target.constructor.name}:${propertyName}`;
descriptor.value = async function(...args) {
const cacheKey = `${cacheKeyPrefix}:${JSON.stringify(args)}`;
const cached = await cacheService.get(cacheKey);
if (cached !== null) {
return cached;
}
const result = await method.apply(this, args);
await cacheService.set(cacheKey, result, ttlSeconds);
return result;
};
};
}
安全最佳实践
输入验证和净化
javascript
const validator = require('validator');
const rateLimit = require('express-rate-limit');
// 输入验证中间件
const validateUserInput = (req, res, next) => {
const { name, email, password } = req.body;
// 验证姓名
if (name && (!validator.isLength(name, { min: 1, max: 50 }) ||
validator.contains(name, '<script>'))) {
return res.status(400).json({ error: '无效的姓名格式' });
}
// 验证邮箱
if (email && !validator.isEmail(email)) {
return res.status(400).json({ error: '无效的邮箱格式' });
}
// 验证密码强度
if (password && !validator.isLength(password, { min: 8 })) {
return res.status(400).json({ error: '密码长度至少为8位' });
}
next();
};
// 速率限制
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 限制每个IP 15分钟内最多100个请求
message: '请求过于频繁,请稍后再试',
standardHeaders: true,
legacyHeaders: false,
});
// 针对登录的特殊限制
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 5, // 限制每个IP 15分钟内最多5次登录尝试
message: '登录尝试次数过多,请15分钟后再试',
skipSuccessfulRequests: true
});
身份验证和授权
javascript
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
class AuthService {
constructor() {
this.jwtSecret = process.env.JWT_SECRET;
this.refreshSecret = process.env.REFRESH_TOKEN_SECRET;
}
async generateTokens(user) {
const payload = {
id: user.id,
email: user.email,
role: user.role
};
const accessToken = jwt.sign(payload, this.jwtSecret, {
expiresIn: '15m',
issuer: 'my-app',
audience: user.id.toString()
});
const refreshToken = jwt.sign(
{ ...payload, type: 'refresh' },
this.refreshSecret,
{ expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
async verifyToken(token, tokenType = 'access') {
try {
const secret = tokenType === 'access' ? this.jwtSecret : this.refreshSecret;
return jwt.verify(token, secret);
} catch (error) {
throw new Error('无效的令牌');
}
}
async hashPassword(password) {
const saltRounds = 12;
return await bcrypt.hash(password, saltRounds);
}
async verifyPassword(password, hashedPassword) {
return await bcrypt.compare(password, hashedPassword);
}
}
// 认证中间件
const authenticate = async (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: '访问令牌缺失' });
}
try {
const decoded = await authService.verifyToken(token);
req.user = decoded;
next();
} catch (error) {
return res.status(403).json({ error: '无效的访问令牌' });
}
};
// 授权中间件
const requireRole = (roles) => {
return (req, res, next) => {
if (!req.user || !roles.includes(req.user.role)) {
return res.status(403).json({ error: '权限不足' });
}
next();
};
};
测试最佳实践
测试组织结构
javascript
// tests/unit/user.service.test.js
const UserService = require('../../src/services/UserService');
const User = require('../../src/models/User');
// 模拟数据库
jest.mock('../../src/models/User');
describe('UserService', () => {
let userService;
beforeEach(() => {
userService = new UserService();
jest.clearAllMocks();
});
describe('createUser', () => {
test('应该成功创建用户', async () => {
const userData = {
name: '测试用户',
email: 'test@example.com',
password: 'password123'
};
const mockUser = {
id: 1,
...userData,
createdAt: new Date()
};
User.create.mockResolvedValue(mockUser);
const result = await userService.createUser(userData);
expect(User.create).toHaveBeenCalledWith({
name: '测试用户',
email: 'test@example.com',
password: expect.any(String) // 密码应该被哈希
});
expect(result).toEqual(mockUser);
});
test('应该验证邮箱格式', async () => {
const invalidUserData = {
name: '测试用户',
email: 'invalid-email',
password: 'password123'
};
await expect(userService.createUser(invalidUserData))
.rejects
.toThrow('无效的邮箱格式');
});
});
});
// tests/integration/auth.test.js
const request = require('supertest');
const app = require('../../src/app');
describe('Authentication API', () => {
test('POST /api/auth/login 应该验证凭据并返回令牌', async () => {
const response = await request(app)
.post('/api/auth/login')
.send({
email: 'test@example.com',
password: 'password123'
})
.expect(200);
expect(response.body).toHaveProperty('accessToken');
expect(response.body).toHaveProperty('refreshToken');
});
});
测试数据工厂
javascript
// tests/factories/user.factory.js
const bcrypt = require('bcryptjs');
const createUser = (overrides = {}) => {
return {
name: overrides.name || '测试用户',
email: overrides.email || `test${Date.now()}@example.com`,
password: overrides.password || 'password123',
role: overrides.role || 'user',
...overrides
};
};
const createHashedUser = async (overrides = {}) => {
const user = createUser(overrides);
const hashedPassword = await bcrypt.hash(user.password, 12);
return {
...user,
password: hashedPassword
};
};
module.exports = {
createUser,
createHashedUser
};
部署最佳实践
环境配置
javascript
// src/config/index.js
const path = require('path');
class Config {
constructor() {
this.env = process.env.NODE_ENV || 'development';
this.isDev = this.env === 'development';
this.isProd = this.env === 'production';
this.isTest = this.env === 'test';
this.app = {
port: parseInt(process.env.PORT) || 3000,
host: process.env.HOST || '0.0.0.0',
name: process.env.APP_NAME || 'My Node App',
version: process.env.APP_VERSION || '1.0.0'
};
this.db = {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT) || 5432,
name: process.env.DB_NAME || 'myapp',
user: process.env.DB_USER || 'user',
password: process.env.DB_PASSWORD || 'password',
ssl: this.isProd
};
this.jwt = {
secret: this.getRequiredEnv('JWT_SECRET'),
expiresIn: process.env.JWT_EXPIRES_IN || '1d',
refreshSecret: this.getRequiredEnv('REFRESH_TOKEN_SECRET')
};
this.redis = {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT) || 6379,
password: process.env.REDIS_PASSWORD
};
}
getRequiredEnv(key) {
if (this.isProd && !process.env[key]) {
throw new Error(`生产环境必需的环境变量 ${key} 未设置`);
}
return process.env[key];
}
validate() {
if (this.isProd) {
const required = ['DB_PASSWORD', 'JWT_SECRET', 'REFRESH_TOKEN_SECRET'];
const missing = required.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(`生产环境缺少必需的环境变量: ${missing.join(', ')}`);
}
}
}
}
const config = new Config();
config.validate();
module.exports = config;
健康检查和监控
javascript
// src/health-check.js
const os = require('os');
const cluster = require('cluster');
class HealthCheck {
constructor(database, cache) {
this.database = database;
this.cache = cache;
}
async check() {
const startTime = Date.now();
const checks = await Promise.allSettled([
this.checkDatabase(),
this.checkCache(),
this.checkSystemResources(),
this.checkApplication()
]);
const results = {
status: 'pass',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
responseTime: Date.now() - startTime,
checks: {
database: this.formatCheckResult(checks[0]),
cache: this.formatCheckResult(checks[1]),
system: this.formatCheckResult(checks[2]),
application: this.formatCheckResult(checks[3])
}
};
// 如果有任何检查失败,将整体状态设为fail
if (checks.some(result => result.status === 'rejected')) {
results.status = 'fail';
}
return results;
}
async checkDatabase() {
const start = Date.now();
await this.database.authenticate();
return {
status: 'pass',
responseTime: Date.now() - start
};
}
async checkCache() {
const start = Date.now();
await this.cache.set('health-check', 'ok', 1);
const value = await this.cache.get('health-check');
return {
status: value === 'ok' ? 'pass' : 'fail',
responseTime: Date.now() - start
};
}
checkSystemResources() {
const memory = process.memoryUsage();
const heapUsedPercent = (memory.heapUsed / memory.heapTotal) * 100;
const load = os.loadavg();
return {
status: heapUsedPercent < 80 && load[0] < 2.0 ? 'pass' : 'warn',
memory: {
heapUsedPercent: Math.round(heapUsedPercent),
usage: memory
},
load: load
};
}
checkApplication() {
return {
status: 'pass',
pid: process.pid,
ppid: process.ppid,
title: process.title
};
}
formatCheckResult(result) {
if (result.status === 'fulfilled') {
return result.value;
} else {
return {
status: 'fail',
error: result.reason.message
};
}
}
}
module.exports = HealthCheck;
日志和监控
结构化日志
javascript
// src/utils/logger.js
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
),
defaultMeta: { service: 'my-node-app' },
transports: [
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
new winston.transports.File({
filename: 'logs/combined.log',
maxsize: 5242880, // 5MB
maxFiles: 5
})
]
});
// 在生产环境中添加日志服务
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}
// 日志中间件
const requestLogger = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('HTTP Request', {
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('User-Agent'),
timestamp: new Date().toISOString()
});
});
next();
};
module.exports = { logger, requestLogger };
代码质量最佳实践
代码审查检查清单
- [ ] 代码遵循项目编码规范
- [ ] 函数和方法有适当的注释
- [ ] 错误处理完整且适当
- [ ] 输入验证充分
- [ ] 没有硬编码的敏感信息
- [ ] 数据库查询经过优化
- [ ] 有适当的单元测试覆盖
- [ ] 性能影响已考虑
- [ ] 安全漏洞已处理
代码质量工具配置
json
// package.json
{
"scripts": {
"lint": "eslint src/**/*.js",
"lint:fix": "eslint src/**/*.js --fix",
"format": "prettier --write src/**/*.js",
"test": "jest",
"test:coverage": "jest --coverage",
"security": "npm audit --audit-level moderate",
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.js": [
"eslint --fix",
"prettier --write",
"git add"
]
}
}
通过遵循这些最佳实践,可以构建出高质量、可维护、安全且高性能的Node.js应用程序。