JavaScript 代码规模增大后,需要一种机制来拆分文件、管理依赖、复用代码,这就是 模块化(Module System)

目前 JavaScript 主要有两种模块规范:

  • CommonJS(CJS)
  • ECMAScript Modules(ESM)

一、CommonJS(CJS)

CommonJS 是 Node.js 早期使用的模块规范

在 ES6 模块化出现之前,Node.js 使用 CommonJS 解决模块化问题。

1 导出模块

1
2
3
4
module.exports = {
add,
sub
}

1
exports.add = add

tips:

  • module.exports 是真正的导出对象
  • exports 只是 module.exports 的引用

2 导入模块

1
const math = require('./math')

tips:
require() 会加载模块并返回 module.exports


3 CommonJS 特点

1️⃣ 同步加载

1
const module = require('./module')

加载流程:

读取文件
↓
执行文件
↓
返回 exports

特点:

  • 同步执行
  • 适合服务器环境

2️⃣ 运行时加载

require() 本质是函数,因此可以动态执行。

1
2
3
if (condition) {
const module = require('./module')
}

3️⃣ 值拷贝

CommonJS 导出的是 值的拷贝

1
2
3
4
5
6
// a.js
let count = 0

module.exports = {
count
}
1
2
3
4
// b.js
const { count } = require('./a')

console.log(count)

如果 a.js 里的 count 改变:

b.js 不会自动更新

4️⃣ 模块缓存

Node.js 会缓存加载过的模块。

1
2
require('./a')
require('./a')

模块只会执行一次。

缓存机制:

Module Cache

二、ESM(ECMAScript Modules)

ESM 是 ES6(2015)官方模块化规范

现在浏览器和 Node.js 都支持。


1 导出模块

命名导出

1
2
export const add = () => {}
export const sub = () => {}

默认导出

1
export default function add() {}

2 导入模块

命名导入

1
import { add } from './math.js'

默认导入

1
import add from './math.js'

全部导入

1
import * as math from './math.js'

3 ESM 特点

1️⃣ 静态分析(编译时解析)

ESM 的依赖关系在 编译阶段就能确定

1
import { add } from './math'

限制:

  • 必须写在顶层
  • 不能写在 if / for / function 中

错误示例:

1
2
3
if (true) {
import { add } from './math'
}

2️⃣ 异步加载

浏览器加载模块时,并行下载模块,适合前端环境。


3️⃣ 实时绑定(引用)

ESM 导出的是 引用(Live Binding)

1
2
3
4
5
6
// a.js
export let count = 0

setTimeout(() => {
count++
}, 1000)
1
2
3
4
// b.js
import { count } from './a'

console.log(count)

count 会随着 a.js 的变化而变化。


4️⃣ 支持 Tree Shaking

由于 ESM 是 静态结构,打包工具可以删除未使用代码。

1
2
export const add = () => {}
export const sub = () => {}

如果只使用:

1
import { add } from './math'

打包时,sub 会被删除。


三、CJS vs ESM 对比

特性 CommonJS ESM
出现时间 Node.js ES6
导入 require import
导出 module.exports export
加载方式 同步 异步
执行时机 运行时 编译时
值类型 值拷贝 引用
Tree Shaking 不支持 支持
浏览器支持 不支持 支持

四、Node.js 中使用 ESM

Node.js 默认使用 CommonJS。如果要使用 ESM,有两种方式:

方式一:package.json

1
2
3
{
"type": "module"
}

之后 .js 文件默认使用 ESM。


方式二:使用 .mjs

index.mjs

五、CJS 和 ESM 互相导入

在 Node.js 中,两种模块系统可能共存。

1 ESM 导入 CJS

1
import pkg from './cjs.js'

默认导出为 module.exports


2 CJS 导入 ESM

不能直接使用 require(),需要:

1
import('./esm.js')

动态导入。


六、ESM 对前端工程化的意义

现代打包工具更偏向使用 ESM,例如:

  • Vite
  • Rollup
  • ESBuild
  • Next.js

原因如下:

1 Tree Shaking

删除未使用代码。

2 静态依赖分析

可以在编译阶段构建依赖图。

3 更好的代码分割

1
import('./module')

七、总结

CommonJS 和 ESM 的区别:
CommonJS 是 Node.js 早期使用的模块规范,使用 requiremodule.exports,模块是同步加载,运行时解析,导出的是值拷贝。
ESM 是 ES6 官方模块规范,使用 importexport,模块在编译阶段进行静态分析,支持异步加载,导出的是引用,并且支持Tree Shaking。

现代前端构建工具(如 Vite、Rollup)通常优先使用ESM,因为它可以在构建阶段进行更好的优化。