Node.js 模块系统

章节介绍

Node.js 的模块系统是其核心特性之一,它让开发者能够将代码组织成可重用的模块。通过模块系统,我们可以将复杂的应用程序分解为多个小文件,每个文件负责特定的功能。本教程将详细介绍 Node.js 的模块系统,帮助您掌握模块化编程的核心概念。

核心知识点

模块系统概述

Node.js 使用 CommonJS 模块规范,每个 JavaScript 文件都被视为一个独立的模块。模块系统提供了以下功能:

  1. 代码封装:每个模块都有自己的作用域,不会污染全局命名空间
  2. 依赖管理:可以明确声明和加载依赖的模块
  3. 代码重用:通过导出和导入实现代码重用
  4. 可维护性:模块化使代码更易于维护和测试
模块系统工作流程:

┌─────────────┐
│  主程序      │
│  (main.js)  │
└──────┬──────┘
       │ require()
       ↓
┌─────────────┐
│  模块 A      │
│ (moduleA.js)│
└──────┬──────┘
       │ require()
       ↓
┌─────────────┐
│  模块 B      │
│ (moduleB.js)│
└─────────────┘

require() 函数

require() 函数用于加载模块。

// 加载核心模块
const fs = require('fs');
const http = require('http');
const path = require('path');

// 加载文件模块
const myModule = require('./myModule.js');
const utils = require('./utils');

// 加载目录模块(会查找目录下的 index.js)
const myPackage = require('./myPackage');

// 加载 node_modules 中的第三方模块
const express = require('express');
const lodash = require('lodash');

// 加载 JSON 文件
const config = require('./config.json');
const package = require('./package.json');

module.exports 和 exports

module.exportsexports 用于导出模块的内容。

// 方式 1:直接赋值给 module.exports
module.exports = {
  name: '张三',
  age: 25,
  greet() {
    console.log('你好!');
  }
};

// 方式 2:使用 exports(注意:不能直接赋值)
exports.name = '李四';
exports.age = 30;
exports.greet = function() {
  console.log('你好!');
};

// 方式 3:混合使用
exports.name = '王五';
exports.age = 28;
module.exports.greet = function() {
  console.log('你好!');
};

// 方式 4:导出函数
module.exports = function(name) {
  console.log(`你好,${name}!`);
};

// 方式 5:导出类
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  greet() {
    console.log(`你好,我是 ${this.name}`);
  }
}

module.exports = Person;

模块导出方式对比

// math.js - 导出多个函数
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;
exports.multiply = (a, b) => a * b;
exports.divide = (a, b) => a / b;

// 使用
const math = require('./math');
console.log(math.add(5, 3));       // 8
console.log(math.subtract(5, 3));  // 2

// utils.js - 导出单个对象
module.exports = {
  formatDate(date) {
    return date.toISOString();
  },
  
  capitalize(str) {
    return str.charAt(0).toUpperCase() + str.slice(1);
  }
};

// 使用
const utils = require('./utils');
console.log(utils.formatDate(new Date()));
console.log(utils.capitalize('hello'));

// logger.js - 导出类
class Logger {
  constructor(name) {
    this.name = name;
  }
  
  log(message) {
    console.log(`[${this.name}] ${message}`);
  }
  
  error(message) {
    console.error(`[${this.name}] ERROR: ${message}`);
  }
}

module.exports = Logger;

// 使用
const Logger = require('./logger');
const logger = new Logger('App');
logger.log('应用启动');
logger.error('发生错误');

模块查找机制

Node.js 按照以下顺序查找模块:

模块查找顺序:

1. 核心模块(fs, http, path 等)
   ↓
2. 相对路径(./ 或 ../)
   ↓
3. 绝对路径(/path/to/module)
   ↓
4. node_modules 目录
   - 当前目录的 node_modules
   - 父目录的 node_modules
   - 继续向上查找,直到根目录
   ↓
5. 全局安装的模块
// 1. 核心模块
const fs = require('fs');

// 2. 相对路径
const utils = require('./utils');
const helper = require('../helper/helper.js');

// 3. 绝对路径
const moduleA = require('/path/to/moduleA');

// 4. node_modules
const express = require('express');

// 5. 目录模块(查找 index.js)
const myPackage = require('./myPackage');

// 6. 不带扩展名(自动查找 .js, .json, .node)
const config = require('./config');  // 会查找 config.js, config.json, config.node

模块缓存

Node.js 会缓存已加载的模块,提高性能。

// counter.js
let count = 0;

module.exports = {
  increment() {
    count++;
    return count;
  },
  
  getCount() {
    return count;
  }
};

// app.js
const counter1 = require('./counter');
const counter2 = require('./counter');

console.log(counter1 === counter2);  // true(同一个模块实例)

console.log(counter1.increment());  // 1
console.log(counter2.increment());  // 2
console.log(counter1.getCount());   // 2
console.log(counter2.getCount());   // 2

// 清除模块缓存
delete require.cache[require.resolve('./counter')];
const counter3 = require('./counter');
console.log(counter3.getCount());   // 0(新的模块实例)

模块作用域

每个模块都有自己的作用域,变量不会污染全局。

// moduleA.js
const privateVar = '私有变量';
let counter = 0;

function privateFunction() {
  console.log('私有函数');
}

module.exports = {
  getCounter() {
    return counter;
  },
  
  increment() {
    counter++;
  },
  
  // 可以访问私有变量
  logPrivate() {
    console.log(privateVar);
    privateFunction();
  }
};

// app.js
const moduleA = require('./moduleA');

// console.log(privateVar);  // 错误:无法访问私有变量
// privateFunction();        // 错误:无法访问私有函数

moduleA.increment();
console.log(moduleA.getCounter());  // 1
moduleA.logPrivate();  // 私有变量 \n 私有函数

实用案例分析

案例 1:创建工具函数模块

创建一个实用的工具函数模块。

// utils/string.js
module.exports = {
  capitalize(str) {
    if (!str) return '';
    return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
  },
  
  truncate(str, length = 50) {
    if (!str || str.length <= length) return str;
    return str.slice(0, length) + '...';
  },
  
  reverse(str) {
    if (!str) return '';
    return str.split('').reverse().join('');
  },
  
  isEmail(str) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(str);
  },
  
  isPhone(str) {
    const phoneRegex = /^1[3-9]\d{9}$/;
    return phoneRegex.test(str);
  }
};

// utils/array.js
module.exports = {
  unique(arr) {
    return [...new Set(arr)];
  },
  
  chunk(arr, size) {
    const chunks = [];
    for (let i = 0; i < arr.length; i += size) {
      chunks.push(arr.slice(i, i + size));
    }
    return chunks;
  },
  
  shuffle(arr) {
    const shuffled = [...arr];
    for (let i = shuffled.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
    }
    return shuffled;
  },
  
  groupBy(arr, key) {
    return arr.reduce((groups, item) => {
      const groupKey = item[key];
      if (!groups[groupKey]) {
        groups[groupKey] = [];
      }
      groups[groupKey].push(item);
      return groups;
    }, {});
  }
};

// utils/date.js
module.exports = {
  format(date, format = 'YYYY-MM-DD HH:mm:ss') {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');
    const hours = String(date.getHours()).padStart(2, '0');
    const minutes = String(date.getMinutes()).padStart(2, '0');
    const seconds = String(date.getSeconds()).padStart(2, '0');
    
    return format
      .replace('YYYY', year)
      .replace('MM', month)
      .replace('DD', day)
      .replace('HH', hours)
      .replace('mm', minutes)
      .replace('ss', seconds);
  },
  
  addDays(date, days) {
    const result = new Date(date);
    result.setDate(result.getDate() + days);
    return result;
  },
  
  diffDays(date1, date2) {
    const oneDay = 24 * 60 * 60 * 1000;
    return Math.round((date2 - date1) / oneDay);
  },
  
  isWeekend(date) {
    const day = date.getDay();
    return day === 0 || day === 6;
  }
};

// utils/index.js
const stringUtils = require('./string');
const arrayUtils = require('./array');
const dateUtils = require('./date');

module.exports = {
  ...stringUtils,
  ...arrayUtils,
  ...dateUtils
};

// app.js
const utils = require('./utils');

console.log(utils.capitalize('hello'));  // Hello
console.log(utils.truncate('This is a very long string', 10));  // This is a...
console.log(utils.unique([1, 2, 2, 3, 3, 3]));  // [1, 2, 3]
console.log(utils.format(new Date()));  // 2024-01-29 12:34:56

案例 2:创建配置管理模块

创建一个配置管理模块,支持不同环境的配置。

// config/default.js
module.exports = {
  app: {
    name: 'My App',
    version: '1.0.0',
    port: 3000
  },
  
  database: {
    host: 'localhost',
    port: 5432,
    name: 'myapp',
    username: 'user',
    password: 'password'
  },
  
  redis: {
    host: 'localhost',
    port: 6379,
    db: 0
  },
  
  jwt: {
    secret: 'your-secret-key',
    expiresIn: '7d'
  },
  
  logging: {
    level: 'info',
    file: 'logs/app.log'
  }
};

// config/development.js
module.exports = {
  app: {
    port: 3000
  },
  
  database: {
    host: 'localhost',
    name: 'myapp_dev'
  },
  
  logging: {
    level: 'debug'
  }
};

// config/production.js
module.exports = {
  app: {
    port: 80
  },
  
  database: {
    host: 'prod-db.example.com',
    name: 'myapp_prod'
  },
  
  redis: {
    host: 'prod-redis.example.com'
  },
  
  logging: {
    level: 'warn'
  }
};

// config/index.js
const defaultConfig = require('./default');
const env = process.env.NODE_ENV || 'development';

try {
  const envConfig = require(`./${env}`);
  module.exports = {
    ...defaultConfig,
    ...envConfig
  };
} catch (error) {
  module.exports = defaultConfig;
}

// app.js
const config = require('./config');

console.log('应用配置:');
console.log(config);

console.log('\n数据库配置:');
console.log(config.database);

console.log('\n当前环境:', process.env.NODE_ENV || 'development');

案例 3:创建数据库操作模块

创建一个数据库操作模块,封装常用的数据库操作。

// database/connection.js
const { Pool } = require('pg');
const config = require('../config');

class DatabaseConnection {
  constructor() {
    this.pool = new Pool({
      host: config.database.host,
      port: config.database.port,
      database: config.database.name,
      user: config.database.username,
      password: config.database.password
    });
  }
  
  async query(text, params) {
    const client = await this.pool.connect();
    try {
      const result = await client.query(text, params);
      return result.rows;
    } finally {
      client.release();
    }
  }
  
  async transaction(callback) {
    const client = await this.pool.connect();
    try {
      await client.query('BEGIN');
      const result = await callback(client);
      await client.query('COMMIT');
      return result;
    } catch (error) {
      await client.query('ROLLBACK');
      throw error;
    } finally {
      client.release();
    }
  }
  
  async close() {
    await this.pool.end();
  }
}

module.exports = new DatabaseConnection();

// database/users.js
const db = require('./connection');

class UserRepository {
  async create({ name, email, password }) {
    const result = await db.query(
      'INSERT INTO users (name, email, password) VALUES ($1, $2, $3) RETURNING *',
      [name, email, password]
    );
    return result[0];
  }
  
  async findById(id) {
    const result = await db.query(
      'SELECT * FROM users WHERE id = $1',
      [id]
    );
    return result[0];
  }
  
  async findByEmail(email) {
    const result = await db.query(
      'SELECT * FROM users WHERE email = $1',
      [email]
    );
    return result[0];
  }
  
  async update(id, updates) {
    const fields = Object.keys(updates);
    const values = Object.values(updates);
    const setClause = fields.map((field, index) => `${field} = $${index + 2}`).join(', ');
    
    const result = await db.query(
      `UPDATE users SET ${setClause} WHERE id = $1 RETURNING *`,
      [id, ...values]
    );
    return result[0];
  }
  
  async delete(id) {
    const result = await db.query(
      'DELETE FROM users WHERE id = $1 RETURNING *',
      [id]
    );
    return result[0];
  }
  
  async findAll(limit = 10, offset = 0) {
    const result = await db.query(
      'SELECT * FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2',
      [limit, offset]
    );
    return result;
  }
}

module.exports = new UserRepository();

// app.js
const userRepository = require('./database/users');

async function main() {
  try {
    // 创建用户
    const user = await userRepository.create({
      name: '张三',
      email: 'zhangsan@example.com',
      password: 'hashed_password'
    });
    console.log('创建用户:', user);
    
    // 查找用户
    const foundUser = await userRepository.findById(user.id);
    console.log('查找用户:', foundUser);
    
    // 更新用户
    const updatedUser = await userRepository.update(user.id, {
      name: '李四'
    });
    console.log('更新用户:', updatedUser);
    
    // 删除用户
    const deletedUser = await userRepository.delete(user.id);
    console.log('删除用户:', deletedUser);
  } catch (error) {
    console.error('错误:', error);
  }
}

main();

代码示例

示例 1:创建计算器模块

// calculator.js
class Calculator {
  add(a, b) {
    return a + b;
  }
  
  subtract(a, b) {
    return a - b;
  }
  
  multiply(a, b) {
    return a * b;
  }
  
  divide(a, b) {
    if (b === 0) {
      throw new Error('除数不能为零');
    }
    return a / b;
  }
  
  power(base, exponent) {
    return Math.pow(base, exponent);
  }
  
  sqrt(number) {
    if (number < 0) {
      throw new Error('不能计算负数的平方根');
    }
    return Math.sqrt(number);
  }
}

module.exports = new Calculator();

// app.js
const calculator = require('./calculator');

console.log(calculator.add(5, 3));       // 8
console.log(calculator.subtract(5, 3));  // 2
console.log(calculator.multiply(5, 3));  // 15
console.log(calculator.divide(6, 3));    // 2
console.log(calculator.power(2, 3));     // 8
console.log(calculator.sqrt(16));        // 4

示例 2:创建日志模块

// logger.js
const fs = require('fs');
const path = require('path');

class Logger {
  constructor(name, options = {}) {
    this.name = name;
    this.level = options.level || 'info';
    this.logFile = options.logFile || null;
    this.levels = {
      error: 0,
      warn: 1,
      info: 2,
      debug: 3
    };
  }
  
  formatMessage(level, message) {
    const timestamp = new Date().toISOString();
    return `[${timestamp}] [${this.name}] [${level.toUpperCase()}] ${message}`;
  }
  
  log(level, message) {
    if (this.levels[level] > this.levels[this.level]) {
      return;
    }
    
    const formattedMessage = this.formatMessage(level, message);
    
    switch (level) {
      case 'error':
        console.error(formattedMessage);
        break;
      case 'warn':
        console.warn(formattedMessage);
        break;
      case 'debug':
        console.debug(formattedMessage);
        break;
      default:
        console.log(formattedMessage);
    }
    
    if (this.logFile) {
      this.writeToFile(formattedMessage);
    }
  }
  
  writeToFile(message) {
    fs.appendFileSync(this.logFile, message + '\n', 'utf8');
  }
  
  error(message) {
    this.log('error', message);
  }
  
  warn(message) {
    this.log('warn', message);
  }
  
  info(message) {
    this.log('info', message);
  }
  
  debug(message) {
    this.log('debug', message);
  }
}

module.exports = Logger;

// app.js
const Logger = require('./logger');

const appLogger = new Logger('App', {
  level: 'debug',
  logFile: 'logs/app.log'
});

const dbLogger = new Logger('Database', {
  level: 'warn',
  logFile: 'logs/database.log'
});

appLogger.info('应用启动');
appLogger.debug('调试信息');
appLogger.warn('警告信息');
appLogger.error('错误信息');

dbLogger.info('数据库连接');  // 不会输出(level 是 warn)
dbLogger.warn('查询缓慢');
dbLogger.error('连接失败');

示例 3:创建验证器模块

// validator.js
class Validator {
  static isEmail(value) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(value);
  }
  
  static isPhone(value) {
    const phoneRegex = /^1[3-9]\d{9}$/;
    return phoneRegex.test(value);
  }
  
  static isUrl(value) {
    const urlRegex = /^https?:\/\/.+/;
    return urlRegex.test(value);
  }
  
  static isNumber(value) {
    return !isNaN(parseFloat(value)) && isFinite(value);
  }
  
  static isInteger(value) {
    return Number.isInteger(Number(value));
  }
  
  static isPositive(value) {
    return this.isNumber(value) && Number(value) > 0;
  }
  
  static isLength(value, min, max) {
    const length = String(value).length;
    return length >= min && length <= max;
  }
  
  static isRequired(value) {
    return value !== null && value !== undefined && value !== '';
  }
  
  static isInRange(value, min, max) {
    return this.isNumber(value) && value >= min && value <= max;
  }
  
  static matches(value, pattern) {
    return pattern.test(value);
  }
}

module.exports = Validator;

// app.js
const Validator = require('./validator');

console.log(Validator.isEmail('test@example.com'));  // true
console.log(Validator.isEmail('invalid-email'));    // false

console.log(Validator.isPhone('13800138000'));      // true
console.log(Validator.isPhone('12345678901'));       // false

console.log(Validator.isUrl('https://example.com')); // true
console.log(Validator.isUrl('example.com'));        // false

console.log(Validator.isNumber('123'));             // true
console.log(Validator.isNumber('abc'));             // false

console.log(Validator.isInteger('123'));             // true
console.log(Validator.isInteger('123.45'));          // false

console.log(Validator.isPositive('10'));            // true
console.log(Validator.isPositive('-5'));            // false

console.log(Validator.isLength('hello', 3, 10));    // true
console.log(Validator.isLength('hi', 3, 10));        // false

console.log(Validator.isRequired('value'));         // true
console.log(Validator.isRequired(''));              // false

console.log(Validator.isInRange('5', 1, 10));       // true
console.log(Validator.isInRange('15', 1, 10));      // false

实现技巧与注意事项

模块设计原则

  1. 单一职责:每个模块只负责一个功能
  2. 高内聚低耦合:模块内部功能相关,模块间依赖最小
  3. 清晰的接口:提供清晰的导出接口
  4. 文档完善:为模块提供使用文档

模块导出最佳实践

  1. 优先使用 module.exports:避免混淆 exports 和 module.exports
  2. 导出对象或类:而不是直接导出函数
  3. 保持一致性:统一使用一种导出方式
  4. 避免循环依赖:模块间不要相互依赖

模块加载优化

  1. 按需加载:只在需要时加载模块
  2. 利用缓存:Node.js 会缓存已加载的模块
  3. 避免重复加载:同一个模块只会被加载一次
  4. 使用相对路径:提高模块查找效率

常见问题与解决方案

问题 1:循环依赖

// moduleA.js
const moduleB = require('./moduleB');

module.exports = {
  name: 'Module A',
  greet() {
    console.log('Hello from Module A');
    moduleB.greet();
  }
};

// moduleB.js
const moduleA = require('./moduleA');

module.exports = {
  name: 'Module B',
  greet() {
    console.log('Hello from Module B');
    moduleA.greet();
  }
};

// app.js
const moduleA = require('./moduleA');
moduleA.greet();  // 可能导致错误或不完整的结果

// 解决方案:重构代码,避免循环依赖
// 或者延迟加载依赖

问题 2:exports 和 module.exports 混淆

// 问题代码
exports.name = 'Module';
module.exports = {
  greet() {
    console.log('Hello');
  }
};

// app.js
const myModule = require('./module');
console.log(myModule.name);  // undefined(exports 被覆盖)

// 解决方案:只使用 module.exports
module.exports = {
  name: 'Module',
  greet() {
    console.log('Hello');
  }
};

// 或者只使用 exports
exports.name = 'Module';
exports.greet = function() {
  console.log('Hello');
};

问题 3:模块路径问题

// 问题代码
const utils = require('utils');  // 错误:没有指定路径

// 解决方案:使用相对路径
const utils = require('./utils');

// 或者使用绝对路径
const path = require('path');
const utils = require(path.join(__dirname, 'utils'));

// 或者将模块放在 node_modules 中
const utils = require('utils');  // 会查找 node_modules/utils

问题 4:模块缓存导致的问题

// counter.js
let count = 0;
module.exports = {
  increment() {
    count++;
    return count;
  }
};

// app.js
const counter1 = require('./counter');
const counter2 = require('./counter');

counter1.increment();  // 1
counter2.increment();  // 2(同一个实例)

// 解决方案:如果需要独立实例,使用工厂函数
// counter.js
module.exports = function() {
  let count = 0;
  return {
    increment() {
      count++;
      return count;
    }
  };
};

// app.js
const createCounter = require('./counter');
const counter1 = createCounter();
const counter2 = createCounter();

counter1.increment();  // 1
counter2.increment();  // 1(独立实例)

总结

本教程详细介绍了 Node.js 的模块系统,包括 CommonJS 模块规范、require 和 module.exports 的使用方法、模块查找机制和模块缓存等重要内容。模块系统是 Node.js 编程的核心概念,掌握它对于开发可维护的 Node.js 应用至关重要。

通过本集的学习,您应该能够:

  1. 理解 Node.js 模块系统的工作原理
  2. 使用 require() 加载各种类型的模块
  3. 使用 module.exports 和 exports 导出模块
  4. 理解模块查找机制和缓存机制
  5. 创建可重用的模块
  6. 避免常见的模块使用错误

在下一集中,我们将学习 Node.js 的全局对象与全局变量,这是理解 Node.js 运行环境的重要一步。继续加油,您的 Node.js 技能正在不断提升!

« 上一篇 JavaScript ES6+ 新特性 下一篇 » Node.js 全局对象与全局变量