Skip to content
On this page

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应用程序。