模块化基础: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 中的解析规则

  1. 相对路径:以 ./ 或 ../ 开头,从当前文件所在目录开始查找。
  2. 绝对路径:以 / 开头,从文件系统根目录查找(很少用)。
  3. 裸模块名:没有路径前缀,比如 import 'lodash',从 node_modules 中查找。
  4. 文件扩展名:CommonJS 会自动补全 .js、.json、.node;ES Module 必须写完整扩展名(除非配置了 resolve)。
  5. 目录解析:如果路径指向一个目录,会查找该目录下的 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())。

模块化知识体系

下面这张图帮你理清整个模块化的脉络:

模块化知识体系结构图 模块化知识体系 CommonJS ES Module 运行时加载(同步) require() / module.exports 值拷贝(浅拷贝) Node.js 默认规范 不支持动态导入 编译时加载(静态分析) import / export 动态绑定(实时引用) 浏览器 + Node.js 标准 支持 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 的区别,你写材料库的时候就能更清晰地设计接口,避免那些「为什么导入不了」的尴尬问题。嗯,接下来咱们就进入实战,看看怎么用模块化思想组织你的材料库代码。