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 模块加载遵循以下顺序:
- 核心模块(如
fs、path) - 文件模块(
./、../或/开头的路径) - 目录模块(查找
package.json的main字段或index.js) 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。
使用方式:
- 文件扩展名为
.mjs - 或
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');
模块系统最佳实践
-
组织项目结构:
/project /lib # 可复用模块 /config # 配置文件 /routes # 路由模块 /models # 数据模型 app.js # 主入口 package.json -
避免副作用:
// 不好的实践 - 模块有副作用 require('./module'); // 模块内部直接执行代码 // 好的实践 - 显式初始化 const module = require('./module'); module.init(); -
控制模块大小:
- 保持模块功能单一
- 避免过大的模块文件(建议 < 500 行)
-
命名规范:
- 使用小写和连字符(
my-module.js) - 类使用大驼峰(
MyClass.js)
- 使用小写和连字符(
-
性能考虑:
- 避免在热路径中频繁
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 模块系统演进
-
ES Modules 成为主流:
- 逐步从 CommonJS 过渡到 ES Modules
- 顶级
await支持
-
加载器钩子(Loader Hooks):
// 实验性功能 - 自定义模块加载行为 import { register } from 'node:module'; register('./custom-loader.js', import.meta.url); -
更多模块类型支持:
- WASM 模块
- JSON 模块
- 原生插件模块
总结对比表
| 特性 | CommonJS (require) |
ES Modules (import) |
|---|---|---|
| 加载方式 | 同步加载 | 异步加载 |
| 运行时态 | 动态解析 | 静态解析 |
| 导出方式 | module.exports |
export 关键字 |
| 导入方式 | require() |
import 语句 |
| 作用域 | 函数作用域 | 模块作用域 |
| 循环依赖处理 | 部分支持 | 完全支持 |
| 浏览器支持 | 需要打包 | 原生支持 |
| 动态导入 | 原生支持 | 通过 import() 支持 |
| Node.js 支持 | 完全支持 | 需要 .mjs 或配置 |
通过深入理解 Node.js 的模块系统和 require 机制,开发者可以更好地组织代码结构,优化应用性能,并顺利过渡到现代 JavaScript 模块标准。
#前端开发
分享于 2025-03-25