Skip to content

项目结构

概述

正确组织 Node.js 项目对于可维护性、可扩展性和团队协作至关重要。本章介绍 Node.js 应用程序的结构化最佳实践,从简单脚本到复杂的企业级应用程序。

基础项目结构

简单应用程序结构

对于小型应用或学习项目:

my-node-app/
├── package.json
├── package-lock.json
├── .gitignore
├── .env
├── README.md
├── app.js                 # Main application file
├── config/
│   └── database.js        # Configuration files
├── routes/
│   ├── index.js          # Route definitions
│   └── users.js
├── models/
│   └── user.js           # Data models
├── views/
│   └── index.html        # Templates (if using)
├── public/
│   ├── css/
│   ├── js/
│   └── images/
└── tests/
    └── app.test.js       # Test files

中型应用程序结构

对于生产应用程序:

enterprise-app/
├── package.json
├── package-lock.json
├── .gitignore
├── .env.example
├── .eslintrc.js
├── .prettierrc
├── README.md
├── Dockerfile
├── docker-compose.yml
├── src/
│   ├── app.js            # Application entry point
│   ├── server.js         # Server setup
│   ├── config/
│   │   ├── index.js      # Configuration management
│   │   ├── database.js
│   │   └── redis.js
│   ├── controllers/
│   │   ├── authController.js
│   │   └── userController.js
│   ├── middleware/
│   │   ├── auth.js
│   │   ├── validation.js
│   │   └── errorHandler.js
│   ├── models/
│   │   ├── index.js
│   │   ├── User.js
│   │   └── Product.js
│   ├── routes/
│   │   ├── index.js
│   │   ├── auth.js
│   │   └── api/
│   │       ├── v1/
│   │       │   ├── index.js
│   │       │   ├── users.js
│   │       │   └── products.js
│   ├── services/
│   │   ├── authService.js
│   │   ├── emailService.js
│   │   └── paymentService.js
│   ├── utils/
│   │   ├── logger.js
│   │   ├── helpers.js
│   │   └── constants.js
│   └── validators/
│       ├── userValidator.js
│       └── productValidator.js
├── tests/
│   ├── unit/
│   ├── integration/
│   └── fixtures/
├── docs/
│   ├── api.md
│   └── deployment.md
├── scripts/
│   ├── seed.js
│   └── migrate.js
└── logs/
    └── .gitkeep

配置管理

基于环境的配置

创建健壮的配置系统:

javascript
// src/config/index.js
const dotenv = require('dotenv');
const path = require('path');

// Load environment variables
dotenv.config();

const config = {
  // Environment
  NODE_ENV: process.env.NODE_ENV || 'development',
  
  // Server
  PORT: parseInt(process.env.PORT, 10) || 3000,
  HOST: process.env.HOST || 'localhost',
  
  // Database
  DATABASE: {
    HOST: process.env.DB_HOST || 'localhost',
    PORT: parseInt(process.env.DB_PORT, 10) || 5432,
    NAME: process.env.DB_NAME || 'myapp',
    USER: process.env.DB_USER || 'postgres',
    PASSWORD: process.env.DB_PASSWORD || '',
    SSL: process.env.DB_SSL === 'true'
  },
  
  // Redis
  REDIS: {
    HOST: process.env.REDIS_HOST || 'localhost',
    PORT: parseInt(process.env.REDIS_PORT, 10) || 6379,
    PASSWORD: process.env.REDIS_PASSWORD || ''
  },
  
  // JWT
  JWT: {
    SECRET: process.env.JWT_SECRET || 'your-secret-key',
    EXPIRES_IN: process.env.JWT_EXPIRES_IN || '24h'
  },
  
  // Email
  EMAIL: {
    SERVICE: process.env.EMAIL_SERVICE || 'gmail',
    USER: process.env.EMAIL_USER || '',
    PASSWORD: process.env.EMAIL_PASSWORD || ''
  },
  
  // File uploads
  UPLOAD: {
    MAX_SIZE: parseInt(process.env.UPLOAD_MAX_SIZE, 10) || 5 * 1024 * 1024, // 5MB
    ALLOWED_TYPES: (process.env.UPLOAD_ALLOWED_TYPES || 'jpg,jpeg,png,pdf').split(',')
  },
  
  // Logging
  LOG_LEVEL: process.env.LOG_LEVEL || 'info'
};

// Validation
function validateConfig() {
  const required = ['JWT.SECRET'];
  
  for (const key of required) {
    const keys = key.split('.');
    let value = config;
    
    for (const k of keys) {
      value = value[k];
    }
    
    if (!value) {
      throw new Error(`Missing required configuration: ${key}`);
    }
  }
}

// Validate in production
if (config.NODE_ENV === 'production') {
  validateConfig();
}

module.exports = config;

环境文件

创建不同的环境文件:

bash
# .env.example
NODE_ENV=development
PORT=3000
HOST=localhost

# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=myapp_dev
DB_USER=postgres
DB_PASSWORD=password
DB_SSL=false

# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=

# JWT
JWT_SECRET=your-super-secret-jwt-key
JWT_EXPIRES_IN=24h

# Email
EMAIL_SERVICE=gmail
EMAIL_USER=your-email@gmail.com
EMAIL_PASSWORD=your-app-password

# File uploads
UPLOAD_MAX_SIZE=5242880
UPLOAD_ALLOWED_TYPES=jpg,jpeg,png,pdf

# Logging
LOG_LEVEL=debug

应用程序入口点

服务器设置

javascript
// src/server.js
const app = require('./app');
const config = require('./config');
const logger = require('./utils/logger');

const server = app.listen(config.PORT, config.HOST, () => {
  logger.info(`Server running on ${config.HOST}:${config.PORT} in ${config.NODE_ENV} mode`);
});

// Graceful shutdown
process.on('SIGTERM', () => {
  logger.info('SIGTERM received. Shutting down gracefully...');
  server.close(() => {
    logger.info('Process terminated');
    process.exit(0);
  });
});

process.on('SIGINT', () => {
  logger.info('SIGINT received. Shutting down gracefully...');
  server.close(() => {
    logger.info('Process terminated');
    process.exit(0);
  });
});

// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
  logger.error('Uncaught Exception:', error);
  process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
  logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
  process.exit(1);
});

module.exports = server;

应用程序设置

javascript
// src/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');
const rateLimit = require('express-rate-limit');

const config = require('./config');
const logger = require('./utils/logger');
const routes = require('./routes');
const errorHandler = require('./middleware/errorHandler');

const app = express();

// Security middleware
app.use(helmet());
app.use(cors({
  origin: config.NODE_ENV === 'production' ? ['https://yourdomain.com'] : true,
  credentials: true
}));

// Rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP'
});
app.use('/api/', limiter);

// Body parsing middleware
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// Compression
app.use(compression());

// Request logging
app.use((req, res, next) => {
  logger.info(`${req.method} ${req.path} - ${req.ip}`);
  next();
});

// Static files
app.use(express.static('public'));

// Routes
app.use('/', routes);

// 404 handler
app.use('*', (req, res) => {
  res.status(404).json({
    success: false,
    message: 'Route not found'
  });
});

// Error handling middleware
app.use(errorHandler);

module.exports = app;

模块化架构

控制器

javascript
// src/controllers/userController.js
const userService = require('../services/userService');
const { validationResult } = require('express-validator');
const logger = require('../utils/logger');

class UserController {
  async getAllUsers(req, res, next) {
    try {
      const { page = 1, limit = 10, search } = req.query;
      
      const users = await userService.getAllUsers({
        page: parseInt(page),
        limit: parseInt(limit),
        search
      });
      
      res.json({
        success: true,
        data: users,
        pagination: {
          page: parseInt(page),
          limit: parseInt(limit),
          total: users.total
        }
      });
    } catch (error) {
      next(error);
    }
  }

  async getUserById(req, res, next) {
    try {
      const { id } = req.params;
      const user = await userService.getUserById(id);
      
      if (!user) {
        return res.status(404).json({
          success: false,
          message: 'User not found'
        });
      }
      
      res.json({
        success: true,
        data: user
      });
    } catch (error) {
      next(error);
    }
  }

  async createUser(req, res, next) {
    try {
      // Check validation errors
      const errors = validationResult(req);
      if (!errors.isEmpty()) {
        return res.status(400).json({
          success: false,
          message: 'Validation failed',
          errors: errors.array()
        });
      }

      const userData = req.body;
      const user = await userService.createUser(userData);
      
      logger.info(`User created: ${user.id}`);
      
      res.status(201).json({
        success: true,
        data: user,
        message: 'User created successfully'
      });
    } catch (error) {
      next(error);
    }
  }

  async updateUser(req, res, next) {
    try {
      const { id } = req.params;
      const updateData = req.body;
      
      const user = await userService.updateUser(id, updateData);
      
      if (!user) {
        return res.status(404).json({
          success: false,
          message: 'User not found'
        });
      }
      
      res.json({
        success: true,
        data: user,
        message: 'User updated successfully'
      });
    } catch (error) {
      next(error);
    }
  }

  async deleteUser(req, res, next) {
    try {
      const { id } = req.params;
      const deleted = await userService.deleteUser(id);
      
      if (!deleted) {
        return res.status(404).json({
          success: false,
          message: 'User not found'
        });
      }
      
      res.json({
        success: true,
        message: 'User deleted successfully'
      });
    } catch (error) {
      next(error);
    }
  }
}

module.exports = new UserController();

服务

javascript
// src/services/userService.js
const User = require('../models/User');
const bcrypt = require('bcrypt');
const logger = require('../utils/logger');

class UserService {
  async getAllUsers({ page = 1, limit = 10, search }) {
    try {
      const offset = (page - 1) * limit;
      let whereClause = {};
      
      if (search) {
        whereClause = {
          $or: [
            { name: { $regex: search, $options: 'i' } },
            { email: { $regex: search, $options: 'i' } }
          ]
        };
      }
      
      const [users, total] = await Promise.all([
        User.find(whereClause)
          .select('-password')
          .skip(offset)
          .limit(limit)
          .sort({ createdAt: -1 }),
        User.countDocuments(whereClause)
      ]);
      
      return {
        users,
        total,
        pages: Math.ceil(total / limit)
      };
    } catch (error) {
      logger.error('Error fetching users:', error);
      throw error;
    }
  }

  async getUserById(id) {
    try {
      const user = await User.findById(id).select('-password');
      return user;
    } catch (error) {
      logger.error(`Error fetching user ${id}:`, error);
      throw error;
    }
  }

  async createUser(userData) {
    try {
      // Check if user already exists
      const existingUser = await User.findOne({ email: userData.email });
      if (existingUser) {
        throw new Error('User with this email already exists');
      }
      
      // Hash password
      const saltRounds = 12;
      const hashedPassword = await bcrypt.hash(userData.password, saltRounds);
      
      // Create user
      const user = new User({
        ...userData,
        password: hashedPassword
      });
      
      await user.save();
      
      // Return user without password
      const { password, ...userWithoutPassword } = user.toObject();
      return userWithoutPassword;
    } catch (error) {
      logger.error('Error creating user:', error);
      throw error;
    }
  }

  async updateUser(id, updateData) {
    try {
      // Remove sensitive fields from update
      const { password, ...safeUpdateData } = updateData;
      
      const user = await User.findByIdAndUpdate(
        id,
        safeUpdateData,
        { new: true, runValidators: true }
      ).select('-password');
      
      return user;
    } catch (error) {
      logger.error(`Error updating user ${id}:`, error);
      throw error;
    }
  }

  async deleteUser(id) {
    try {
      const user = await User.findByIdAndDelete(id);
      return !!user;
    } catch (error) {
      logger.error(`Error deleting user ${id}:`, error);
      throw error;
    }
  }

  async getUserByEmail(email) {
    try {
      const user = await User.findOne({ email });
      return user;
    } catch (error) {
      logger.error(`Error fetching user by email ${email}:`, error);
      throw error;
    }
  }
}

module.exports = new UserService();

中间件

javascript
// src/middleware/auth.js
const jwt = require('jsonwebtoken');
const config = require('../config');
const userService = require('../services/userService');

const authMiddleware = async (req, res, next) => {
  try {
    const token = req.header('Authorization')?.replace('Bearer ', '');
    
    if (!token) {
      return res.status(401).json({
        success: false,
        message: 'Access denied. No token provided.'
      });
    }
    
    const decoded = jwt.verify(token, config.JWT.SECRET);
    const user = await userService.getUserById(decoded.userId);
    
    if (!user) {
      return res.status(401).json({
        success: false,
        message: 'Invalid token. User not found.'
      });
    }
    
    req.user = user;
    next();
  } catch (error) {
    res.status(401).json({
      success: false,
      message: 'Invalid token.'
    });
  }
};

const adminMiddleware = (req, res, next) => {
  if (req.user.role !== 'admin') {
    return res.status(403).json({
      success: false,
      message: 'Access denied. Admin privileges required.'
    });
  }
  next();
};

module.exports = {
  authMiddleware,
  adminMiddleware
};

工具和辅助函数

日志记录器

javascript
// src/utils/logger.js
const winston = require('winston');
const config = require('../config');

const logger = winston.createLogger({
  level: config.LOG_LEVEL,
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: { service: 'node-app' },
  transports: [
    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
    new winston.transports.File({ filename: 'logs/combined.log' })
  ]
});

// Add console transport in development
if (config.NODE_ENV !== 'production') {
  logger.add(new winston.transports.Console({
    format: winston.format.combine(
      winston.format.colorize(),
      winston.format.simple()
    )
  }));
}

module.exports = logger;

辅助函数

javascript
// src/utils/helpers.js
const crypto = require('crypto');

/**
 * Generate random string
 */
function generateRandomString(length = 32) {
  return crypto.randomBytes(length).toString('hex');
}

/**
 * Sanitize user input
 */
function sanitizeInput(input) {
  if (typeof input !== 'string') return input;
  return input.trim().replace(/[<>]/g, '');
}

/**
 * Format response
 */
function formatResponse(success, data = null, message = null, errors = null) {
  const response = { success };
  
  if (data !== null) response.data = data;
  if (message !== null) response.message = message;
  if (errors !== null) response.errors = errors;
  
  return response;
}

/**
 * Pagination helper
 */
function getPaginationData(page, limit, total) {
  const totalPages = Math.ceil(total / limit);
  const hasNext = page < totalPages;
  const hasPrev = page > 1;
  
  return {
    page,
    limit,
    total,
    totalPages,
    hasNext,
    hasPrev
  };
}

/**
 * Async wrapper for route handlers
 */
function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

module.exports = {
  generateRandomString,
  sanitizeInput,
  formatResponse,
  getPaginationData,
  asyncHandler
};

Package.json 脚本

json
{
  "name": "node-enterprise-app",
  "version": "1.0.0",
  "description": "Enterprise Node.js application",
  "main": "src/server.js",
  "scripts": {
    "start": "node src/server.js",
    "dev": "nodemon src/server.js",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "lint": "eslint src/",
    "lint:fix": "eslint src/ --fix",
    "format": "prettier --write src/",
    "seed": "node scripts/seed.js",
    "migrate": "node scripts/migrate.js",
    "build": "echo 'No build step required'",
    "docker:build": "docker build -t node-app .",
    "docker:run": "docker run -p 3000:3000 node-app"
  },
  "dependencies": {
    "express": "^4.18.2",
    "mongoose": "^7.0.0",
    "bcrypt": "^5.1.0",
    "jsonwebtoken": "^9.0.0",
    "cors": "^2.8.5",
    "helmet": "^6.0.1",
    "compression": "^1.7.4",
    "express-rate-limit": "^6.7.0",
    "express-validator": "^6.15.0",
    "winston": "^3.8.2",
    "dotenv": "^16.0.3"
  },
  "devDependencies": {
    "nodemon": "^2.0.22",
    "jest": "^29.5.0",
    "supertest": "^6.3.3",
    "eslint": "^8.38.0",
    "prettier": "^2.8.7"
  }
}

下一步

在下一章中,我们将详细探索配置管理,并学习如何有效地处理不同的环境。

实践练习

  1. 为博客应用程序创建项目结构
  2. 实现带验证的配置系统
  3. 构建模块化身份验证系统
  4. 创建用于日志记录和错误处理的可重用中间件

关键要点

  • 适当的项目结构提高可维护性和可扩展性
  • 使用控制器、服务和中间件分离关注点
  • 为不同的部署场景使用基于环境的配置
  • 实施适当的错误处理和日志记录
  • 创建可重用的实用工具和辅助函数
  • 遵循一致的命名约定和文件组织

本站内容仅供学习和研究使用。