从零构建一个现代化 Node.js 项目:架构、工具与最佳实践

3 阅读1分钟

在 Node.js 生态快速发展的今天,如何搭建一个既健壮又易于维护的现代 Node.js 项目?本文将带你从零开始,深入探讨现代 Node.js 项目的架构设计、工具链配置和最佳实践,帮助你构建出符合生产标准的应用程序。

为什么需要现代化的项目结构?

随着项目规模的增长,一个良好的项目结构能够:

  1. 提高代码可维护性:清晰的目录结构让团队成员快速理解代码组织
  2. 增强可测试性:模块化设计便于单元测试和集成测试
  3. 便于团队协作:统一的代码规范和工具链减少协作成本
  4. 支持持续集成/部署:标准化的配置简化 DevOps 流程

项目初始化与基础配置

1. 项目初始化

# 创建项目目录
mkdir modern-node-app
cd modern-node-app

# 初始化 package.json
npm init -y

# 使用 TypeScript(推荐)
npm install typescript @types/node --save-dev
npx tsc --init

2. 基础目录结构

modern-node-app/
├── src/
│   ├── core/           # 核心业务逻辑
│   ├── modules/        # 功能模块
│   ├── shared/         # 共享代码
│   ├── config/         # 配置文件
│   ├── middleware/     # 中间件
│   └── app.ts         # 应用入口
├── tests/             # 测试文件
├── scripts/           # 构建/部署脚本
├── docs/              # 文档
├── .env.example       # 环境变量示例
├── .gitignore
├── package.json
└── tsconfig.json

核心架构设计

分层架构模式

现代 Node.js 项目通常采用分层架构,确保关注点分离:

// src/core/application/Application.ts
export abstract class Application {
  protected abstract setupMiddleware(): void;
  protected abstract setupRoutes(): void;
  protected abstract setupErrorHandling(): void;
  
  public async initialize(): Promise<void> {
    await this.setupDatabase();
    this.setupMiddleware();
    this.setupRoutes();
    this.setupErrorHandling();
  }
  
  protected async setupDatabase(): Promise<void> {
    // 数据库连接逻辑
  }
}

依赖注入容器

使用依赖注入提高代码的可测试性和可维护性:

// src/core/di/Container.ts
export class Container {
  private services = new Map<string, any>();
  
  register<T>(key: string, factory: () => T): void {
    this.services.set(key, factory);
  }
  
  resolve<T>(key: string): T {
    const factory = this.services.get(key);
    if (!factory) {
      throw new Error(`Service ${key} not found`);
    }
    return factory();
  }
}

// 使用示例
const container = new Container();
container.register('userService', () => new UserService());
const userService = container.resolve<UserService>('userService');

现代化工具链配置

1. TypeScript 配置优化

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "moduleResolution": "node",
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@core/*": ["src/core/*"],
      "@modules/*": ["src/modules/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "tests"]
}

2. ESLint + Prettier 代码规范

// .eslintrc.js
module.exports = {
  parser: '@typescript-eslint/parser',
  plugins: ['@typescript-eslint', 'prettier'],
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier'
  ],
  rules: {
    'prettier/prettier': 'error',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/no-unused-vars': ['error', { 
      'argsIgnorePattern': '^_',
      'varsIgnorePattern': '^_' 
    }]
  },
  env: {
    node: true,
    es2022: true
  }
};

3. 热重载开发体验

// package.json scripts 部分
{
  "scripts": {
    "dev": "nodemon --watch src --ext ts,json --exec \"ts-node src/app.ts\"",
    "build": "tsc",
    "start": "node dist/app.js",
    "test": "jest",
    "lint": "eslint src/**/*.ts",
    "format": "prettier --write src/**/*.ts",
    "type-check": "tsc --noEmit"
  }
}

模块化设计实践

用户模块示例

// src/modules/users/user.module.ts
export class UserModule {
  constructor(
    private readonly userService: UserService,
    private readonly userController: UserController,
    private readonly userRepository: UserRepository
  ) {}
  
  async initialize(): Promise<void> {
    // 模块初始化逻辑
  }
}

// src/modules/users/user.service.ts
export class UserService {
  constructor(private readonly userRepository: UserRepository) {}
  
  async createUser(userData: CreateUserDto): Promise<User> {
    // 业务逻辑验证
    if (await this.userRepository.existsByEmail(userData.email)) {
      throw new Error('Email already exists');
    }
    
    // 密码加密
    const hashedPassword = await this.hashPassword(userData.password);
    
    // 创建用户
    const user = await this.userRepository.create({
      ...userData,
      password: hashedPassword
    });
    
    // 发送欢迎邮件(异步)
    this.sendWelcomeEmail(user.email).catch(console.error);
    
    return user;
  }
  
  private async hashPassword(password: string): Promise<string> {
    // 使用 bcrypt 等库进行密码哈希
    return password; // 简化示例
  }
  
  private async sendWelcomeEmail(email: string): Promise<void> {
    // 邮件发送逻辑
  }
}

错误处理与日志记录

统一的错误处理中间件

// src/middleware/error.middleware.ts
import { Request, Response, NextFunction } from 'express';

export class ErrorMiddleware {
  static handle(
    error: Error,
    req: Request,
    res: Response,
    next: NextFunction
  ): void {
    // 记录错误日志
    console.error(`[${new Date().toISOString()}] Error:`, {
      message: error.message,
      stack: error.stack,
      path: req.path,
      method: req.method,
      ip: req.ip
    });
    
    // 根据错误类型返回不同的状态码
    const statusCode = this.getStatusCode(error);
    const response = this.formatErrorResponse(error, statusCode);
    
    res.status(statusCode).json(response);
  }
  
  private static getStatusCode(error: Error): number {
    // 根据错误类型返回相应的 HTTP 状态码
    if (error.name === 'ValidationError') return 400;
    if (error.name === 'UnauthorizedError') return 401;
    if (error.name === 'NotFoundError') return 404;
    return 500;
  }
  
  private static formatErrorResponse(
    error: Error, 
    statusCode: number
  ): object {
    const response: any = {
      success: false,
      error: error.message,
      timestamp: new Date().toISOString()
    };
    
    // 开发环境返回堆栈信息
    if (process.env.NODE_ENV === 'development') {
      response.stack = error.stack;
    }
    
    return response;
  }
}

结构化日志记录

// src/core/logging/Logger.ts
import winston from 'winston';

export class Logger {
  private static instance: winston.Logger;
  
  static getInstance(): winston.Logger {
    if (!Logger.instance) {
      Logger.instance = winston.createLogger({
        level: process.env.LOG_LEVEL || 'info',
        format: winston.format.combine(
          winston.format.timestamp(),
          winston.format.errors({ stack