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 };
});| 参数 | 说明 |
|---|---|
exports | module.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.node2.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/lodash3. 模块缓存机制 🔥
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); // true3.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 避免循环依赖
- 重构: 提取共享代码到第三方模块
- 依赖注入: 通过参数传递依赖
- 延迟加载: 在函数内部 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 区别?
exports是module.exports的引用- 直接赋值
exports = {}会切断引用,无效 - 最终导出的永远是
module.exports
Q2: require 的缓存机制?
- 模块第一次加载后缓存在
require.cache - 后续 require 直接返回缓存
- 可通过
delete require.cache[path]清除
Q3: CJS 和 ESM 的核心区别?
| 特性 | CJS | ESM |
|---|---|---|
| 加载时机 | 运行时 | 编译时 |
| 导出类型 | 值拷贝 | Live Binding |
| 顶层 this | module.exports | undefined |
| 动态导入 | require() 任意位置 | import() 返回 Promise |
Q4: 如何解决循环依赖?
- 重构代码结构,提取公共模块
- 使用函数包装延迟访问
- 依赖注入模式
Q5: ESM 中如何使用 __dirname?
javascript
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);Q6: require 的解析顺序?
- 核心模块 (fs, path...)
- 相对/绝对路径 (./xxx, /xxx)
- 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"
}
}
}