16.1 模块系统与 require 机制

Node.js 模块系统基础

CommonJS 模块规范

Node.js 最初实现了 CommonJS 模块规范,这是其原生支持的模块系统。

基本导出与导入

// math.js - 模块定义
const add = (a, b) => a + b;
const subtract = (a, b) => a - b;

// 导出多个成员
module.exports = {
  add,
  subtract
};

// 或者逐个导出
// exports.add = add;
// exports.subtract = subtract;

// app.js - 模块使用
const math = require('./math');
console.log(math.add(2, 3)); // 5

模块加载流程

Node.js 模块加载遵循以下顺序:

  1. 核心模块(如 fspath
  2. 文件模块(./..// 开头的路径)
  3. 目录模块(查找 package.jsonmain 字段或 index.js
  4. node_modules 中的模块
// 加载核心模块
const fs = require('fs');

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

// 加载目录模块
const myPackage = require('./my-package');

// 加载node_modules模块
const lodash = require('lodash');

require 机制深度解析

模块缓存机制

Node.js 会对加载的模块进行缓存,避免重复加载。

// moduleA.js
console.log('模块A被加载');
module.exports = 'A';

// app.js
const a1 = require('./moduleA'); // 打印"模块A被加载"
const a2 = require('./moduleA'); // 无输出,直接返回缓存
console.log(a1 === a2); // true

模块包装器

Node.js 在执行模块代码前会将其包装在一个函数中:

(function(exports, require, module, __filename, __dirname) {
  // 模块代码
});

这使得每个模块都有自己独立的作用域。

循环依赖处理

// a.js
console.log('a开始');
exports.done = false;
const b = require('./b');
console.log('在a中,b.done =', b.done);
exports.done = true;
console.log('a结束');

// b.js
console.log('b开始');
exports.done = false;
const a = require('./a');
console.log('在b中,a.done =', a.done);
exports.done = true;
console.log('b结束');

// main.js
console.log('main开始');
const a = require('./a');
const b = require('./b');
console.log('在main中,a.done=', a.done, 'b.done=', b.done);

/*
输出顺序:
main开始
a开始
b开始
在b中,a.done = false
b结束
在a中,b.done = true
a结束
在main中,a.done= true b.done= true
*/

现代 Node.js 模块系统

ES Modules 支持

Node.js 12+ 开始支持原生 ES Modules。

使用方式

  1. 文件扩展名为 .mjs
  2. package.json 中设置 "type": "module"
// math.mjs - ES模块导出
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

// app.mjs - ES模块导入
import { add, subtract } from './math.mjs';
console.log(add(2, 3));

CommonJS 与 ES Modules 互操作

// ES模块导入CommonJS模块
import cjsModule from './commonjs-module.js';
console.log(cjsModule.someValue);

// CommonJS模块导入ES模块(需要动态导入)
async function loadESModule() {
  const esModule = await import('./es-module.mjs');
  console.log(esModule.someValue);
}

高级模块技巧

模块路径解析

// 查看模块解析路径
console.log(require.resolve('lodash'));

// 自定义模块路径
module.paths; // 可以修改此数组添加自定义查找路径

模块热替换模式

// 开发环境下实现模块热更新
function watchModule(modulePath) {
  const fullPath = require.resolve(modulePath);
  delete require.cache[fullPath];
  return require(fullPath);
}

// 使用示例
let myModule = require('./my-module');
setInterval(() => {
  myModule = watchModule('./my-module');
}, 5000);

动态模块加载

// 根据条件动态加载模块
const moduleName = process.env.NODE_ENV === 'production' 
  ? './prod-logger' 
  : './dev-logger';

const logger = require(moduleName);
logger.log('Hello');

模块系统最佳实践

  1. 组织项目结构

    /project
      /lib           # 可复用模块
      /config        # 配置文件
      /routes        # 路由模块
      /models        # 数据模型
      app.js         # 主入口
      package.json
    
  2. 避免副作用

    // 不好的实践 - 模块有副作用
    require('./module'); // 模块内部直接执行代码
    
    // 好的实践 - 显式初始化
    const module = require('./module');
    module.init();
    
  3. 控制模块大小

    • 保持模块功能单一
    • 避免过大的模块文件(建议 < 500 行)
  4. 命名规范

    • 使用小写和连字符(my-module.js
    • 类使用大驼峰(MyClass.js
  5. 性能考虑

    • 避免在热路径中频繁 require
    • 对重型模块考虑延迟加载

常见问题与解决方案

1. 模块未找到错误

解决方案

// 检查模块路径
console.log(require.resolve('module-name'));

// 确保package.json和node_modules正确
// 检查NODE_PATH环境变量

2. 循环依赖问题

解决方案

  • 重构代码避免循环依赖
  • 在必要时使用延迟加载
// a.js
exports.loaded = false;
const b = require('./b');
exports.b = b;
exports.loaded = true;

// b.js
exports.loaded = false;
const a = require('./a');
exports.a = a;
exports.loaded = true;

3. ES Modules 与 CommonJS 混用问题

解决方案

  • 统一使用一种模块系统
  • 必要时使用动态 import() 加载 ES 模块
  • package.json 中明确指定 "type": "module""type": "commonjs"

模块调试技巧

查看模块缓存

// 打印所有缓存的模块
console.log(require.cache);

// 删除特定模块缓存
delete require.cache[require.resolve('./module')];

模块加载时序分析

// --trace-module-loading 标志
// node --trace-module-loading app.js

// 或在代码中添加调试
const oldRequire = require;
require = function(module) {
  console.time('require-' + module);
  const result = oldRequire(module);
  console.timeEnd('require-' + module);
  return result;
};

未来发展趋势

Node.js 模块系统演进

  1. ES Modules 成为主流

    • 逐步从 CommonJS 过渡到 ES Modules
    • 顶级 await 支持
  2. 加载器钩子(Loader Hooks)

    // 实验性功能 - 自定义模块加载行为
    import { register } from 'node:module';
    register('./custom-loader.js', import.meta.url);
    
  3. 更多模块类型支持

    • WASM 模块
    • JSON 模块
    • 原生插件模块

总结对比表

特性 CommonJS (require) ES Modules (import)
加载方式 同步加载 异步加载
运行时态 动态解析 静态解析
导出方式 module.exports export 关键字
导入方式 require() import 语句
作用域 函数作用域 模块作用域
循环依赖处理 部分支持 完全支持
浏览器支持 需要打包 原生支持
动态导入 原生支持 通过 import() 支持
Node.js 支持 完全支持 需要 .mjs 或配置

通过深入理解 Node.js 的模块系统和 require 机制,开发者可以更好地组织代码结构,优化应用性能,并顺利过渡到现代 JavaScript 模块标准。

#前端开发 分享于 2025-03-25

【 内容由 AI 共享,不代表本站观点,请谨慎参考 】