Skip to content

组件和模块

概述

Node.js 应用程序使用模块构建——封装功能的可重用代码片段。本章涵盖如何有效地创建、组织和使用模块,包括 CommonJS 模块、ES6 模块和 npm 包。

模块系统

CommonJS 模块(默认)

CommonJS 是 Node.js 中的传统模块系统:

javascript
// math-utils.js - Exporting functions
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

function multiply(a, b) {
  return a * b;
}

function divide(a, b) {
  if (b === 0) {
    throw new Error('Division by zero');
  }
  return a / b;
}

// Different export patterns
module.exports = {
  add,
  subtract,
  multiply,
  divide
};

// Alternative: individual exports
// exports.add = add;
// exports.subtract = subtract;
javascript
// calculator.js - Exporting a class
class Calculator {
  constructor() {
    this.history = [];
    this.memory = 0;
  }

  calculate(operation, a, b) {
    let result;
    
    switch (operation) {
      case 'add':
        result = a + b;
        break;
      case 'subtract':
        result = a - b;
        break;
      case 'multiply':
        result = a * b;
        break;
      case 'divide':
        if (b === 0) throw new Error('Division by zero');
        result = a / b;
        break;
      default:
        throw new Error('Unknown operation');
    }

    this.history.push({ operation, a, b, result, timestamp: new Date() });
    return result;
  }

  getHistory() {
    return [...this.history];
  }

  clearHistory() {
    this.history = [];
  }

  memoryStore(value) {
    this.memory = value;
  }

  memoryRecall() {
    return this.memory;
  }

  memoryClear() {
    this.memory = 0;
  }
}

module.exports = Calculator;
javascript
// app.js - Using modules
const mathUtils = require('./math-utils');
const Calculator = require('./calculator');

// Using function exports
console.log('Addition:', mathUtils.add(5, 3));
console.log('Subtraction:', mathUtils.subtract(10, 4));

// Using class export
const calc = new Calculator();
console.log('Calculator result:', calc.calculate('multiply', 6, 7));
console.log('History:', calc.getHistory());

ES6 Modules (ESM)

Enable ES6 modules by adding "type": "module" to package.json:

javascript
// math-utils.mjs - Named exports
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

export const PI = 3.14159;

export class MathHelper {
  static square(n) {
    return n * n;
  }

  static cube(n) {
    return n * n * n;
  }
}

// Default export
export default function multiply(a, b) {
  return a * b;
}
javascript
// calculator.mjs - Default and named exports
export default class Calculator {
  constructor() {
    this.value = 0;
  }

  add(n) {
    this.value += n;
    return this;
  }

  subtract(n) {
    this.value -= n;
    return this;
  }

  multiply(n) {
    this.value *= n;
    return this;
  }

  divide(n) {
    if (n === 0) throw new Error('Division by zero');
    this.value /= n;
    return this;
  }

  getValue() {
    return this.value;
  }

  reset() {
    this.value = 0;
    return this;
  }
}

export const CONSTANTS = {
  PI: 3.14159,
  E: 2.71828
};

export function formatResult(value, decimals = 2) {
  return parseFloat(value.toFixed(decimals));
}
javascript
// app.mjs - Using ES6 modules
import multiply, { add, subtract, PI, MathHelper } from './math-utils.mjs';
import Calculator, { CONSTANTS, formatResult } from './calculator.mjs';

// Using named imports
console.log('Addition:', add(5, 3));
console.log('PI value:', PI);
console.log('Square:', MathHelper.square(4));

// Using default import
console.log('Multiplication:', multiply(6, 7));

// Using class
const calc = new Calculator();
const result = calc.add(10).multiply(2).subtract(5).getValue();
console.log('Chained calculation:', formatResult(result));

Creating Reusable Components

Database Connection Module

javascript
// database/connection.js
const mongoose = require('mongoose');
const config = require('../config');

class DatabaseConnection {
  constructor() {
    this.connection = null;
    this.isConnected = false;
  }

  async connect() {
    try {
      if (this.isConnected) {
        return this.connection;
      }

      this.connection = await mongoose.connect(config.database.url, {
        useNewUrlParser: true,
        useUnifiedTopology: true,
        maxPoolSize: config.database.poolSize
      });

      this.isConnected = true;
      console.log('Database connected successfully');

      // Handle connection events
      mongoose.connection.on('error', (error) => {
        console.error('Database connection error:', error);
        this.isConnected = false;
      });

      mongoose.connection.on('disconnected', () => {
        console.log('Database disconnected');
        this.isConnected = false;
      });

      return this.connection;
    } catch (error) {
      console.error('Database connection failed:', error);
      throw error;
    }
  }

  async disconnect() {
    if (this.connection) {
      await mongoose.disconnect();
      this.isConnected = false;
      console.log('Database disconnected');
    }
  }

  getConnection() {
    return this.connection;
  }

  isConnectionActive() {
    return this.isConnected;
  }
}

// Singleton pattern
const dbConnection = new DatabaseConnection();
module.exports = dbConnection;

Logger Module

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

class Logger {
  constructor() {
    this.logger = this.createLogger();
  }

  createLogger() {
    const logFormat = winston.format.combine(
      winston.format.timestamp(),
      winston.format.errors({ stack: true }),
      winston.format.json()
    );

    const transports = [
      new winston.transports.File({
        filename: path.join('logs', 'error.log'),
        level: 'error',
        maxsize: 5242880, // 5MB
        maxFiles: 5
      }),
      new winston.transports.File({
        filename: path.join('logs', 'combined.log'),
        maxsize: 5242880,
        maxFiles: 5
      })
    ];

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

    return winston.createLogger({
      level: config.logging.level,
      format: logFormat,
      transports
    });
  }

  info(message, meta = {}) {
    this.logger.info(message, meta);
  }

  error(message, meta = {}) {
    this.logger.error(message, meta);
  }

  warn(message, meta = {}) {
    this.logger.warn(message, meta);
  }

  debug(message, meta = {}) {
    this.logger.debug(message, meta);
  }

  // HTTP request logging
  logRequest(req, res, responseTime) {
    const logData = {
      method: req.method,
      url: req.url,
      statusCode: res.statusCode,
      responseTime: `${responseTime}ms`,
      userAgent: req.get('User-Agent'),
      ip: req.ip
    };

    if (res.statusCode >= 400) {
      this.error('HTTP Request Error', logData);
    } else {
      this.info('HTTP Request', logData);
    }
  }

  // Database operation logging
  logDatabaseOperation(operation, collection, duration, error = null) {
    const logData = {
      operation,
      collection,
      duration: `${duration}ms`
    };

    if (error) {
      this.error('Database Operation Failed', { ...logData, error: error.message });
    } else {
      this.debug('Database Operation', logData);
    }
  }
}

// Singleton pattern
const logger = new Logger();
module.exports = logger;

Email Service Module

javascript
// services/email-service.js
const nodemailer = require('nodemailer');
const fs = require('fs').promises;
const path = require('path');
const config = require('../config');
const logger = require('../utils/logger');

class EmailService {
  constructor() {
    this.transporter = null;
    this.templates = new Map();
    this.init();
  }

  async init() {
    try {
      // Create transporter
      this.transporter = nodemailer.createTransporter({
        host: config.email.smtp.host,
        port: config.email.smtp.port,
        secure: config.email.smtp.secure,
        auth: config.email.smtp.auth
      });

      // Verify connection
      await this.transporter.verify();
      logger.info('Email service initialized successfully');

      // Load email templates
      await this.loadTemplates();
    } catch (error) {
      logger.error('Email service initialization failed', { error: error.message });
      throw error;
    }
  }

  async loadTemplates() {
    const templatesDir = path.join(__dirname, '../templates/email');
    
    try {
      const templateFiles = await fs.readdir(templatesDir);
      
      for (const file of templateFiles) {
        if (file.endsWith('.html')) {
          const templateName = path.basename(file, '.html');
          const templatePath = path.join(templatesDir, file);
          const templateContent = await fs.readFile(templatePath, 'utf8');
          this.templates.set(templateName, templateContent);
        }
      }
      
      logger.info(`Loaded ${this.templates.size} email templates`);
    } catch (error) {
      logger.warn('Failed to load email templates', { error: error.message });
    }
  }

  async sendEmail(to, subject, template, data = {}) {
    try {
      let html = this.templates.get(template);
      
      if (!html) {
        throw new Error(`Template '${template}' not found`);
      }

      // Simple template variable replacement
      html = this.replaceTemplateVariables(html, data);

      const mailOptions = {
        from: `${config.email.from.name} <${config.email.from.address}>`,
        to,
        subject,
        html
      };

      const result = await this.transporter.sendMail(mailOptions);
      
      logger.info('Email sent successfully', {
        to,
        subject,
        template,
        messageId: result.messageId
      });

      return result;
    } catch (error) {
      logger.error('Failed to send email', {
        to,
        subject,
        template,
        error: error.message
      });
      throw error;
    }
  }

  replaceTemplateVariables(template, data) {
    let result = template;
    
    for (const [key, value] of Object.entries(data)) {
      const regex = new RegExp(`{{${key}}}`, 'g');
      result = result.replace(regex, value);
    }
    
    return result;
  }

  async sendWelcomeEmail(userEmail, userName) {
    return this.sendEmail(
      userEmail,
      'Welcome to Our Platform!',
      'welcome',
      {
        userName,
        loginUrl: `${config.app.url}/login`,
        supportEmail: config.email.from.address
      }
    );
  }

  async sendPasswordResetEmail(userEmail, resetToken) {
    const resetUrl = `${config.app.url}/reset-password?token=${resetToken}`;
    
    return this.sendEmail(
      userEmail,
      'Password Reset Request',
      'reset-password',
      {
        resetUrl,
        expiryTime: '1 hour'
      }
    );
  }
}

module.exports = EmailService;

Cache Module

javascript
// services/cache-service.js
const redis = require('redis');
const config = require('../config');
const logger = require('../utils/logger');

class CacheService {
  constructor() {
    this.client = null;
    this.isConnected = false;
    this.defaultTTL = config.redis.ttl;
  }

  async connect() {
    try {
      this.client = redis.createClient({
        host: config.redis.host,
        port: config.redis.port,
        password: config.redis.password,
        db: config.redis.db
      });

      this.client.on('error', (error) => {
        logger.error('Redis connection error', { error: error.message });
        this.isConnected = false;
      });

      this.client.on('connect', () => {
        logger.info('Redis connected');
        this.isConnected = true;
      });

      this.client.on('disconnect', () => {
        logger.warn('Redis disconnected');
        this.isConnected = false;
      });

      await this.client.connect();
      return this.client;
    } catch (error) {
      logger.error('Failed to connect to Redis', { error: error.message });
      throw error;
    }
  }

  async disconnect() {
    if (this.client) {
      await this.client.disconnect();
      this.isConnected = false;
    }
  }

  generateKey(prefix, identifier) {
    return `${config.redis.keyPrefix}${prefix}:${identifier}`;
  }

  async get(key) {
    try {
      if (!this.isConnected) {
        logger.warn('Cache not available, skipping get operation');
        return null;
      }

      const value = await this.client.get(key);
      
      if (value) {
        logger.debug('Cache hit', { key });
        return JSON.parse(value);
      }
      
      logger.debug('Cache miss', { key });
      return null;
    } catch (error) {
      logger.error('Cache get error', { key, error: error.message });
      return null;
    }
  }

  async set(key, value, ttl = this.defaultTTL) {
    try {
      if (!this.isConnected) {
        logger.warn('Cache not available, skipping set operation');
        return false;
      }

      const serializedValue = JSON.stringify(value);
      await this.client.setEx(key, ttl, serializedValue);
      
      logger.debug('Cache set', { key, ttl });
      return true;
    } catch (error) {
      logger.error('Cache set error', { key, error: error.message });
      return false;
    }
  }

  async delete(key) {
    try {
      if (!this.isConnected) {
        return false;
      }

      const result = await this.client.del(key);
      logger.debug('Cache delete', { key, deleted: result > 0 });
      return result > 0;
    } catch (error) {
      logger.error('Cache delete error', { key, error: error.message });
      return false;
    }
  }

  async exists(key) {
    try {
      if (!this.isConnected) {
        return false;
      }

      const result = await this.client.exists(key);
      return result === 1;
    } catch (error) {
      logger.error('Cache exists error', { key, error: error.message });
      return false;
    }
  }

  async flush() {
    try {
      if (!this.isConnected) {
        return false;
      }

      await this.client.flushDb();
      logger.info('Cache flushed');
      return true;
    } catch (error) {
      logger.error('Cache flush error', { error: error.message });
      return false;
    }
  }

  // Higher-level caching methods
  async cacheFunction(key, fn, ttl = this.defaultTTL) {
    const cachedResult = await this.get(key);
    
    if (cachedResult !== null) {
      return cachedResult;
    }

    const result = await fn();
    await this.set(key, result, ttl);
    return result;
  }

  async invalidatePattern(pattern) {
    try {
      if (!this.isConnected) {
        return false;
      }

      const keys = await this.client.keys(pattern);
      
      if (keys.length > 0) {
        await this.client.del(keys);
        logger.info('Cache pattern invalidated', { pattern, keysDeleted: keys.length });
      }
      
      return true;
    } catch (error) {
      logger.error('Cache pattern invalidation error', { pattern, error: error.message });
      return false;
    }
  }
}

module.exports = CacheService;

Module Organization Patterns

Repository Pattern

javascript
// repositories/base-repository.js
class BaseRepository {
  constructor(model) {
    this.model = model;
  }

  async findById(id) {
    return await this.model.findById(id);
  }

  async findAll(options = {}) {
    const { page = 1, limit = 10, sort = { createdAt: -1 } } = options;
    const skip = (page - 1) * limit;

    const [items, total] = await Promise.all([
      this.model.find().sort(sort).skip(skip).limit(limit),
      this.model.countDocuments()
    ]);

    return {
      items,
      total,
      page,
      limit,
      pages: Math.ceil(total / limit)
    };
  }

  async create(data) {
    const item = new this.model(data);
    return await item.save();
  }

  async update(id, data) {
    return await this.model.findByIdAndUpdate(id, data, { new: true });
  }

  async delete(id) {
    return await this.model.findByIdAndDelete(id);
  }

  async findByField(field, value) {
    return await this.model.find({ [field]: value });
  }

  async exists(id) {
    const count = await this.model.countDocuments({ _id: id });
    return count > 0;
  }
}

module.exports = BaseRepository;
javascript
// repositories/user-repository.js
const BaseRepository = require('./base-repository');
const User = require('../models/User');

class UserRepository extends BaseRepository {
  constructor() {
    super(User);
  }

  async findByEmail(email) {
    return await this.model.findOne({ email });
  }

  async findActiveUsers() {
    return await this.model.find({ isActive: true });
  }

  async updateLastLogin(userId) {
    return await this.model.findByIdAndUpdate(
      userId,
      { lastLoginAt: new Date() },
      { new: true }
    );
  }

  async searchUsers(query, options = {}) {
    const { page = 1, limit = 10 } = options;
    const skip = (page - 1) * limit;

    const searchRegex = new RegExp(query, 'i');
    const filter = {
      $or: [
        { name: searchRegex },
        { email: searchRegex }
      ]
    };

    const [users, total] = await Promise.all([
      this.model.find(filter).skip(skip).limit(limit),
      this.model.countDocuments(filter)
    ]);

    return {
      users,
      total,
      page,
      limit,
      pages: Math.ceil(total / limit)
    };
  }
}

module.exports = UserRepository;

Service Layer Pattern

javascript
// services/user-service.js
const UserRepository = require('../repositories/user-repository');
const EmailService = require('./email-service');
const CacheService = require('./cache-service');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const config = require('../config');
const logger = require('../utils/logger');

class UserService {
  constructor() {
    this.userRepository = new UserRepository();
    this.emailService = new EmailService();
    this.cacheService = new CacheService();
  }

  async createUser(userData) {
    try {
      // Check if user already exists
      const existingUser = await this.userRepository.findByEmail(userData.email);
      if (existingUser) {
        throw new Error('User with this email already exists');
      }

      // Hash password
      const hashedPassword = await bcrypt.hash(userData.password, config.security.bcryptRounds);
      
      // Create user
      const user = await this.userRepository.create({
        ...userData,
        password: hashedPassword,
        isActive: true,
        createdAt: new Date()
      });

      // Send welcome email
      await this.emailService.sendWelcomeEmail(user.email, user.name);

      // Cache user data
      const cacheKey = this.cacheService.generateKey('user', user._id);
      await this.cacheService.set(cacheKey, this.sanitizeUser(user));

      logger.info('User created successfully', { userId: user._id, email: user.email });
      
      return this.sanitizeUser(user);
    } catch (error) {
      logger.error('Failed to create user', { error: error.message, userData: { email: userData.email } });
      throw error;
    }
  }

  async getUserById(userId) {
    try {
      // Try cache first
      const cacheKey = this.cacheService.generateKey('user', userId);
      const cachedUser = await this.cacheService.get(cacheKey);
      
      if (cachedUser) {
        return cachedUser;
      }

      // Fetch from database
      const user = await this.userRepository.findById(userId);
      
      if (!user) {
        return null;
      }

      const sanitizedUser = this.sanitizeUser(user);
      
      // Cache the result
      await this.cacheService.set(cacheKey, sanitizedUser);
      
      return sanitizedUser;
    } catch (error) {
      logger.error('Failed to get user by ID', { userId, error: error.message });
      throw error;
    }
  }

  async authenticateUser(email, password) {
    try {
      const user = await this.userRepository.findByEmail(email);
      
      if (!user) {
        throw new Error('Invalid credentials');
      }

      const isPasswordValid = await bcrypt.compare(password, user.password);
      
      if (!isPasswordValid) {
        throw new Error('Invalid credentials');
      }

      if (!user.isActive) {
        throw new Error('Account is deactivated');
      }

      // Update last login
      await this.userRepository.updateLastLogin(user._id);

      // Generate JWT token
      const token = jwt.sign(
        { userId: user._id, email: user.email },
        config.jwt.secret,
        { expiresIn: config.jwt.expiresIn }
      );

      logger.info('User authenticated successfully', { userId: user._id, email });

      return {
        user: this.sanitizeUser(user),
        token
      };
    } catch (error) {
      logger.error('Authentication failed', { email, error: error.message });
      throw error;
    }
  }

  async updateUser(userId, updateData) {
    try {
      // Remove sensitive fields
      const { password, ...safeUpdateData } = updateData;
      
      const updatedUser = await this.userRepository.update(userId, safeUpdateData);
      
      if (!updatedUser) {
        throw new Error('User not found');
      }

      // Invalidate cache
      const cacheKey = this.cacheService.generateKey('user', userId);
      await this.cacheService.delete(cacheKey);

      logger.info('User updated successfully', { userId });
      
      return this.sanitizeUser(updatedUser);
    } catch (error) {
      logger.error('Failed to update user', { userId, error: error.message });
      throw error;
    }
  }

  async searchUsers(query, options) {
    try {
      const result = await this.userRepository.searchUsers(query, options);
      
      return {
        ...result,
        users: result.users.map(user => this.sanitizeUser(user))
      };
    } catch (error) {
      logger.error('Failed to search users', { query, error: error.message });
      throw error;
    }
  }

  sanitizeUser(user) {
    const { password, ...sanitizedUser } = user.toObject ? user.toObject() : user;
    return sanitizedUser;
  }
}

module.exports = UserService;

NPM Package Creation

Creating a Reusable Package

javascript
// package.json for a utility package
{
  "name": "@mycompany/node-utils",
  "version": "1.0.0",
  "description": "Utility functions for Node.js applications",
  "main": "index.js",
  "module": "index.mjs",
  "types": "index.d.ts",
  "files": [
    "lib/",
    "index.js",
    "index.mjs",
    "index.d.ts",
    "README.md"
  ],
  "scripts": {
    "build": "babel src --out-dir lib",
    "test": "jest",
    "lint": "eslint src/",
    "prepublishOnly": "npm run build && npm test"
  },
  "keywords": ["nodejs", "utilities", "helpers"],
  "author": "Your Name",
  "license": "MIT",
  "dependencies": {},
  "devDependencies": {
    "@babel/cli": "^7.0.0",
    "@babel/core": "^7.0.0",
    "@babel/preset-env": "^7.0.0",
    "jest": "^29.0.0",
    "eslint": "^8.0.0"
  }
}
javascript
// index.js - Main entry point
const { formatDate, formatCurrency } = require('./lib/formatters');
const { validateEmail, validatePhone } = require('./lib/validators');
const { generateId, generateSlug } = require('./lib/generators');
const { asyncRetry, asyncTimeout } = require('./lib/async-utils');

module.exports = {
  formatters: {
    formatDate,
    formatCurrency
  },
  validators: {
    validateEmail,
    validatePhone
  },
  generators: {
    generateId,
    generateSlug
  },
  asyncUtils: {
    asyncRetry,
    asyncTimeout
  }
};

下一步

在下一章中,我们将探索 Node.js 应用程序中的路由和导航,包括 Express.js 路由模式。

实践练习

  1. 创建一个模块化身份验证系统,包含 JWT、OAuth 和本地身份验证的独立模块
  2. 构建一个允许动态加载模块的插件系统
  3. 为通用实用函数创建一个 npm 包
  4. 实现一个带依赖注入的服务层

关键要点

  • 模块是 Node.js 应用程序的构建块
  • CommonJS 和 ES6 模块提供了不同的代码组织方法
  • 仓库模式分离数据访问逻辑
  • 服务层封装业务逻辑
  • 适当的模块组织提高可维护性和可测试性
  • 创建可重用的包促进跨项目的代码共享
  • 依赖注入使模块更具可测试性和灵活性

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