Skip to content

Node.js 模块机制

1. CommonJS (CJS) 详解

1.1 基本语法

javascript
// math.js - 导出
const add = (a, b) => a + b;
const PI = 3.14159;

module.exports = { add, PI };
// 或
exports.add = add;
exports.PI = PI;

// app.js - 导入
const { add, PI } = require('./math');
const math = require('./math');

1.2 模块包装器 (Module Wrapper)

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

javascript
(function(exports, require, module, __filename, __dirname) {
    // 你的模块代码实际运行在这里
    const add = (a, b) => a + b;
    module.exports = { add };
});
参数说明
exportsmodule.exports 的引用
require加载其他模块的函数
module当前模块对象
__filename当前文件的绝对路径
__dirname当前文件所在目录的绝对路径

1.3 exports vs module.exports 🔥

javascript
// ⚠️ 陷阱: exports 只是 module.exports 的引用
exports = { foo: 'bar' };  // ❌ 无效! 切断了引用

// ✅ 正确方式
exports.foo = 'bar';
// 或
module.exports = { foo: 'bar' };
初始状态:
┌─────────────┐     ┌─────────────────┐
│   exports   │────>│ module.exports  │ ────> {}
└─────────────┘     └─────────────────┘

exports = { foo: 1 } 后:
┌─────────────┐     ┌─────────────────┐
│   exports   │────>│   { foo: 1 }    │  (新对象)
└─────────────┘     └─────────────────┘

┌─────────────────┐
│ module.exports  │ ────> {}  (原对象, 这才是真正导出的!)
└─────────────────┘

2. require 解析算法

2.1 解析流程

require(X) from module at path Y


┌─────────────────────────────────────────┐
│ 1. 是否是核心模块? (fs, path, http...)   │
│    → 返回核心模块                        │
└────────────────┬────────────────────────┘
                 │ 否

┌─────────────────────────────────────────┐
│ 2. X 以 './' 或 '../' 或 '/' 开头?      │
│    → 作为文件或目录加载                  │
└────────────────┬────────────────────────┘
                 │ 否

┌─────────────────────────────────────────┐
│ 3. 从 node_modules 目录加载             │
│    → 向上递归查找 node_modules           │
└─────────────────────────────────────────┘

2.2 作为文件加载

LOAD_AS_FILE(X)
1. 如果 X 是文件 → 加载 X (根据扩展名)
2. 如果 X.js 存在 → 加载 X.js
3. 如果 X.json 存在 → 加载 X.json (JSON.parse)
4. 如果 X.node 存在 → 加载 X.node (C++ 插件)

2.3 作为目录加载

LOAD_AS_DIRECTORY(X)
1. 如果 X/package.json 存在:
   - 读取 "main" 字段 → 加载 X/<main>
2. 如果 X/index.js 存在 → 加载 X/index.js
3. 如果 X/index.json 存在 → 加载 X/index.json
4. 如果 X/index.node 存在 → 加载 X/index.node

2.4 node_modules 查找

javascript
// 假设当前文件: /home/user/project/src/app.js
require('lodash');

// 查找顺序:
// 1. /home/user/project/src/node_modules/lodash
// 2. /home/user/project/node_modules/lodash
// 3. /home/user/node_modules/lodash
// 4. /home/node_modules/lodash
// 5. /node_modules/lodash

3. 模块缓存机制 🔥

3.1 缓存策略

javascript
// counter.js
let count = 0;
module.exports = {
    increment: () => ++count,
    getCount: () => count
};

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

counter1.increment();
console.log(counter2.getCount()); // 1 ← 同一个实例!
console.log(counter1 === counter2); // true

3.2 查看和清除缓存

javascript
// 查看缓存
console.log(require.cache);
// {
//   '/path/to/module.js': Module { ... },
//   ...
// }

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

// 清除所有缓存
Object.keys(require.cache).forEach(key => {
    delete require.cache[key];
});

3.3 热重载实现

javascript
function hotRequire(modulePath) {
    const absolutePath = require.resolve(modulePath);
    
    // 清除缓存
    delete require.cache[absolutePath];
    
    // 重新加载
    return require(absolutePath);
}

// 监听文件变化
const fs = require('fs');
fs.watch('./config.js', () => {
    const config = hotRequire('./config');
    console.log('Config reloaded:', config);
});

4. ES Modules (ESM)

4.1 基本语法

javascript
// math.mjs
export const add = (a, b) => a + b;
export const PI = 3.14159;
export default function multiply(a, b) {
    return a * b;
}

// app.mjs
import multiply, { add, PI } from './math.mjs';
import * as math from './math.mjs';

// 动态导入
const module = await import('./math.mjs');

4.2 启用 ESM 的方式

方式说明
.mjs 扩展名文件自动被视为 ESM
package.json"type": "module"目录下所有 .js 视为 ESM
--input-type=module命令行标志
json
// package.json
{
    "type": "module"
}

4.3 ESM 中使用 CJS

javascript
// ESM 中导入 CJS
import cjsModule from './legacy.cjs';
import { createRequire } from 'module';

const require = createRequire(import.meta.url);
const lodash = require('lodash');  // 使用 require

// 获取 __dirname 等价物
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

5. CJS vs ESM 对比 🔥

┌─────────────────────────────────────────────────────────────────────┐
│                     CommonJS vs ES Modules                          │
├─────────────────────────────┬───────────────────────────────────────┤
│          CommonJS           │            ES Modules                 │
├─────────────────────────────┼───────────────────────────────────────┤
│  require() / module.exports │  import / export                      │
│  同步加载                    │  异步加载                             │
│  运行时解析                  │  编译时静态分析                        │
│  值的拷贝 (对象为引用)        │  Live Binding (活绑定)                │
│  可以条件加载               │  import 必须在顶层 (动态用 import())   │
│  默认 Node.js 模块          │  浏览器原生支持                        │
└─────────────────────────────┴───────────────────────────────────────┘

5.1 值的拷贝 vs Live Binding

javascript
// CJS - 值的拷贝
// counter.js
let count = 0;
module.exports = { count, increment: () => ++count };

// app.js
const { count, increment } = require('./counter');
increment();
console.log(count); // 0 ← 不会改变!
javascript
// ESM - Live Binding
// counter.mjs
export let count = 0;
export const increment = () => ++count;

// app.mjs
import { count, increment } from './counter.mjs';
increment();
console.log(count); // 1 ← 实时更新!

5.2 加载时机差异

javascript
// CJS - 运行时加载
if (condition) {
    const module = require('./dynamic');  // ✅ 可以
}

// ESM - 编译时静态分析
if (condition) {
    import module from './dynamic';  // ❌ 语法错误
}

// ESM 动态导入
if (condition) {
    const module = await import('./dynamic');  // ✅ 动态 import
}

6. 循环依赖

6.1 CommonJS 循环依赖

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

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

// main.js
require('./a');

// 输出:
// a.js 开始
// b.js 开始
// 在 b.js 中, a.done = false  ← 获取到不完整的导出!
// b.js 结束
// 在 a.js 中, b.done = true
// a.js 结束

6.2 ESM 循环依赖

javascript
// a.mjs
import { b } from './b.mjs';
export const a = 'a';
console.log('a.mjs:', b);

// b.mjs
import { a } from './a.mjs';
export const b = 'b';
console.log('b.mjs:', a);  // ReferenceError! a 还未初始化

// 解决方案: 使用函数延迟访问
// b.mjs
import { getA } from './a.mjs';
export const b = 'b';
console.log('b.mjs:', getA());  // 执行时 a 已初始化

6.3 避免循环依赖

  1. 重构: 提取共享代码到第三方模块
  2. 依赖注入: 通过参数传递依赖
  3. 延迟加载: 在函数内部 require/import

7. 条件导出 (Conditional Exports)

7.1 package.json exports 字段

json
{
    "name": "my-package",
    "exports": {
        ".": {
            "import": "./esm/index.mjs",
            "require": "./cjs/index.cjs",
            "default": "./cjs/index.cjs"
        },
        "./utils": {
            "import": "./esm/utils.mjs",
            "require": "./cjs/utils.cjs"
        }
    }
}

7.2 子路径导出

json
{
    "exports": {
        ".": "./index.js",
        "./feature": "./src/feature.js",
        "./package.json": "./package.json"
    }
}
javascript
// 使用
import feature from 'my-package/feature';

8. require.resolve 与 Module 对象

8.1 require.resolve

javascript
// 解析模块路径 (不加载)
const path = require.resolve('lodash');
console.log(path);  // '/path/to/node_modules/lodash/lodash.js'

// 检查模块是否存在
try {
    require.resolve('non-existent');
} catch (e) {
    console.log('Module not found');
}

// 指定查找路径
require.resolve('lodash', {
    paths: ['/custom/path']
});

8.2 Module 对象属性

javascript
// 在模块内部访问
console.log(module.id);       // 模块标识符
console.log(module.filename); // 文件绝对路径
console.log(module.loaded);   // 是否加载完成
console.log(module.parent);   // 调用此模块的模块 (已废弃)
console.log(module.children); // 此模块加载的模块
console.log(module.paths);    // node_modules 查找路径

9. 面试高频问题

Q1: exports 和 module.exports 区别?

  • exportsmodule.exports 的引用
  • 直接赋值 exports = {} 会切断引用,无效
  • 最终导出的永远是 module.exports

Q2: require 的缓存机制?

  • 模块第一次加载后缓存在 require.cache
  • 后续 require 直接返回缓存
  • 可通过 delete require.cache[path] 清除

Q3: CJS 和 ESM 的核心区别?

特性CJSESM
加载时机运行时编译时
导出类型值拷贝Live Binding
顶层 thismodule.exportsundefined
动态导入require() 任意位置import() 返回 Promise

Q4: 如何解决循环依赖?

  1. 重构代码结构,提取公共模块
  2. 使用函数包装延迟访问
  3. 依赖注入模式

Q5: ESM 中如何使用 __dirname?

javascript
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

Q6: require 的解析顺序?

  1. 核心模块 (fs, path...)
  2. 相对/绝对路径 (./xxx, /xxx)
  3. node_modules (向上递归查找)

Q7: package.json 中 main 和 exports 的区别?

字段作用优先级
main传统入口点
exports现代导出映射,支持条件导出
json
{
    "main": "./dist/index.js",  // 兼容旧版 Node
    "exports": {
        ".": {
            "import": "./dist/esm/index.js",
            "require": "./dist/cjs/index.js"
        }
    }
}

前端面试知识库