一、模块化基础概念
1.1 什么是模块化
模块化是将程序分解为独立、可重用、可维护的代码单元的开发方式。Node.js 实现了完整的模块系统,解决了:
- 变量污染问题
- 代码组织问题
- 依赖管理问题
1.2 Node.js 模块化发展历程
- CommonJS 规范(2009)
- ES Modules(2015)
- Node.js 对两种规范的支持演进
二、CommonJS 模块系统详解
2.1 核心机制
// 模块定义
module.exports = {
// 导出内容
};
// 或
exports.key = value;
// 模块引入,推荐写相对路径
const mod = require('./module');
2.2 实现原理
Node.js 通过五个步骤处理模块:
- 路径解析:解析模块的绝对路径
- 缓存检查:检查
require.cache - 文件加载:读取文件内容
- 包装执行:使用模块包装器
- 缓存存储:存入
require.cache
2.3 模块包装器
Node.js 实际执行的代码:
// 注意:require引入的是module.exports,所以使用exports = {}这种方式不对
// 为什么exports.key=value可以呢?因为module.exports和exports相等,
//所以exports.key=value把内容添加到了module.exports。
// 但是由于require就认module.exports,所以exports=这种写法不对
(function(module.exports, require, module, __filename, __dirname) {
// 用户模块代码
});
2.4 完整加载流程
- 解析文件路径
- 检查核心模块
- 检查文件模块
- 检查目录模块
- 检查
node_modules - 加载并执行
三、ES Modules 深度解析
3.1 基本语法
// 导出
export const name = 'value';
export default function() {};
// 导入
import { name } from './module.mjs';
import defExport from './module.mjs';
3.2 与 CommonJS 的本质区别
| 特性 | CommonJS | ES Modules |
|---|---|---|
| 加载时机 | 运行时动态加载 | 编译时静态分析 |
| 绑定机制 | 值拷贝 | 实时绑定(live binding) |
| 循环依赖处理 | 部分支持 | 完善支持 |
| 顶层 this | 指向 module.exports | undefined |
| 解析方式 | 同步 | 异步 |
3.3 静态分析特性
ESM 的 import 语句:
- 必须位于模块顶层
- 不能动态生成路径
- 会被 JavaScript 引擎静态分析
四、混合使用双模块系统
4.1 互操作方案
4.1.1 ESM 加载 CJS
// ESM 中
import cjsModule from './commonjs.cjs';
import { method } from './commonjs.cjs'; // 注意命名导入的限制
4.1.2 CJS 加载 ESM
// CJS 中必须使用动态导入
(async () => {
const esmModule = await import('./esm.mjs');
})();
4.2 配置方案
4.2.1 package.json 配置
{
"type": "module", // 默认使用 ESM
"main": "./index.cjs", // CJS 入口
"exports": {
".": {
"require": "./index.cjs", // CJS 入口
"import": "./index.mjs" // ESM 入口
}
}
}
4.2.2 文件扩展名策略
.js:根据 package.json 的 type 决定.cjs:强制 CommonJS.mjs:强制 ESM
五、高级模块特性
5.1 条件导出
{
"exports": {
".": {
"require": "./cjs/index.js",
"import": "./esm/index.js",
"node": "./node-specific.js",
"default": "./fallback.js"
}
}
}
5.2 子路径导出
{
"exports": {
"./feature": "./src/feature.js"
}
}
5.3 加载器钩子(实验性)
// 自定义模块加载行为
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
六、模块解析算法详解
6.1 CommonJS 解析顺序
- 精确文件名匹配
- 尝试添加
.js - 尝试添加
.json - 尝试添加
.node - 作为目录解析
6.2 ESM 解析差异
- 必须明确文件扩展名
- 支持 URL 风格的路径
- 遵循浏览器解析规则
七、性能优化建议
- 模块缓存:理解
require.cache机制 - 循环依赖:避免复杂的相互依赖
- 模块拆分:合理划分模块粒度
- 预加载:使用
--require标志预加载模块
八、调试技巧
8.1 查看模块缓存
console.log(require.cache);
8.2 查看模块解析过程
node --inspect-brk app.js
# 然后调试 require 调用栈
8.3 查看实际加载模块
node -r trace-modules app.js
九、未来发展趋势
- ESM 成为标准:Node.js 正在向 ESM 迁移
- 导入映射(Import Maps) :浏览器风格的依赖解析
- Wasm 模块支持:更高效的模块加载
- 更细粒度的模块加载:按需加载模块部分内容
十、最佳实践总结
- 新项目优先使用 ESM
- 库开发者应提供双模式支持
- 保持模块单一职责
- 合理使用动态导入
- 注意浏览器兼容性需求
- 利用 tree-shaking 优化 ESM