模块化基础:CommonJS 与 ES Module 的区别、import/export 语法、模块解析规则
模块化,说白了就是把你的一大坨代码拆成一个个独立的小文件。每个文件只管自己的事,通过明确的接口对外暴露功能。我刚开始写前端时,项目里全是全局变量,一个函数命名冲突能让你排查半天。后来用了模块化,整个世界清净了。
今天咱们就聊聊 JavaScript 世界里最主流的两种模块规范:CommonJS 和 ES Module。搞懂它们,你写材料库的时候才能游刃有余。
CommonJS 与 ES Module:核心差异
CommonJS 是 Node.js 的默认模块系统,用 require 加载,module.exports 导出。ES Module 是 ES6 引入的官方标准,用 import 和 export。两者看起来差不多,但底层逻辑完全不同。
| 对比维度 | CommonJS | ES Module |
|---|---|---|
| 加载时机 | 运行时加载(同步) | 编译时加载(静态分析) |
| 导出方式 | module.exports / exports |
export / export default |
| 导入方式 | require() |
import ... from ... |
| 值传递 | 浅拷贝(值拷贝) | 动态绑定(实时引用) |
| 异步支持 | 不支持(同步) | 支持 import() 动态导入 |
| 顶层 this | 指向 module.exports |
undefined |
| 循环依赖 | 返回已执行部分的导出 | 抛出引用错误(需注意顺序) |
一句话总结:CommonJS 是「运行时」的,ES Module 是「编译时」的。这个区别决定了它们的行为完全不同。
import/export 语法详解
ES Module 的语法其实很简洁,但有几个细节容易踩坑。我一个个说。
命名导出与默认导出
// 命名导出(一个模块可以有多个)
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
// 默认导出(一个模块只能有一个)
export default class Calculator {
// ...
}
// 也可以统一导出
const multiply = (a, b) => a * b;
const divide = (a, b) => a / b;
export { multiply, divide };
导入方式
// 导入命名导出
import { add, subtract } from './math.js';
// 导入默认导出
import Calculator from './calculator.js';
// 混合导入
import Calculator, { add, subtract } from './math.js';
// 重命名导入
import { add as sum } from './math.js';
// 整体导入(命名空间)
import * as math from './math.js';
math.add(1, 2);
我的习惯:一个模块尽量只用一个默认导出,配合少量命名导出。这样导入时最清晰,别人看你的代码一眼就知道核心功能是什么。
动态导入
ES Module 支持 import() 动态加载,返回一个 Promise。这在按需加载材料库时特别有用。
// 按需加载
button.addEventListener('click', async () => {
const { showModal } = await import('./modal.js');
showModal('加载成功!');
});
模块解析规则
模块解析,就是当你写 import './foo' 时,引擎到底去哪里找这个文件。规则其实不复杂,但不同环境有细微差别。
Node.js 中的解析规则
- 相对路径:以
./或../开头,从当前文件所在目录开始查找。 - 绝对路径:以
/开头,从文件系统根目录查找(很少用)。 - 裸模块名:没有路径前缀,比如
import 'lodash',从node_modules中查找。 - 文件扩展名:CommonJS 会自动补全
.js、.json、.node;ES Module 必须写完整扩展名(除非配置了resolve)。 - 目录解析:如果路径指向一个目录,会查找该目录下的
index.js或package.json中main字段指定的文件。
我曾经踩过的坑:在 Node.js 中使用 ES Module 时,忘记写文件扩展名,结果一直报 ERR_MODULE_NOT_FOUND。后来养成了习惯:ES Module 下永远写完整路径 + 扩展名。
浏览器中的解析规则
浏览器加载 ES Module 时,路径必须是 URL 格式。相对路径相对于当前 HTML 文件的 URL,绝对路径相对于站点根目录。
<script type="module">
import { format } from './utils/format.js';
// 浏览器会发起 HTTP 请求获取这个文件
</script>
CommonJS 与 ES Module 的互操作
实际项目中,你经常需要混用两种模块系统。比如一个用 CommonJS 写的 npm 包,要在 ES Module 项目里使用。
| 场景 | 做法 | 注意事项 |
|---|---|---|
| ES Module 导入 CommonJS | import pkg from 'cjs-pkg' |
只能使用默认导入,命名导入会报错 |
| CommonJS 导入 ES Module | const pkg = await import('esm-pkg') |
必须使用动态 import(),不能用 require |
| CommonJS 导出给 ES Module 用 | module.exports = { ... } |
ES Module 默认导入整个对象 |
核心原则:ES Module 可以 import CommonJS 模块,但 CommonJS 不能 require ES Module 模块(除非用动态 import())。
模块化知识体系
下面这张图帮你理清整个模块化的脉络:
实战建议:如何选择
你可能会问:那我到底该用哪个?我的建议很简单:
- 新项目:优先用 ES Module。它是标准,浏览器原生支持,工具链也完善。
- Node.js 后端:如果项目较老,用 CommonJS 也没问题。新项目建议在
package.json中设置"type": "module"启用 ES Module。 - 材料库开发:我建议输出 ES Module 格式,同时提供 CommonJS 兼容版本(通过构建工具生成)。这样用户无论用什么环境都能用。
一个小技巧:在 package.json 中同时指定 "main"(CommonJS)和 "module"(ES Module)字段,打包工具会自动选择正确的版本。
模块化是构建大型应用的基础。搞懂了 CommonJS 和 ES Module 的区别,你写材料库的时候就能更清晰地设计接口,避免那些「为什么导入不了」的尴尬问题。嗯,接下来咱们就进入实战,看看怎么用模块化思想组织你的材料库代码。