很多刚入门前端的小白可能不了解JavaScript模块化技术进程,下面按时间线把 JavaScript 模块化从"无"到"语言原生支持"的完整过程讲清楚,重点讲:为什么出现 → 解决了什么问题 → 语法/原理 → 优缺点 → 现在怎么用。
一、无模块时代:全局变量 + 多 script(1995--2009)
1)形态
页面里多个 <script> 按顺序引入:
html
<script src="a.js"></script>
<script src="b.js"></script>
所有变量、函数都在 window 全局作用域。
2)痛点
- 全局污染、命名冲突 :两个文件都定义
var name,后面覆盖前面。 - 依赖顺序硬编码:a 依赖 b,就必须 b 先加载,顺序错直接报错。
- 无法管理依赖关系:项目一大,依赖全靠人脑维护。
3)雏形:命名空间 + IIFE
命名空间(Namespace)
js
// user.js
window.app = window.app || {};
app.user = {
name: 'Tom',
getName() { return this.name; }
};
用一个全局对象当"命名空间",减少冲突,但仍然是全局。
IIFE(立即执行函数)------真正的"私有作用域"
js
// module.js
(function() {
// 私有,外部访问不到
let secret = 'xxx';
function foo() {}
// 暴露接口到全局
window.myModule = { foo };
})();
优点:变量私有、不污染全局 。
缺点:无法声明依赖、不能自动加载依赖。
二、CommonJS:服务器端模块化(2009,Node.js)
1)背景
2009 年 Node.js 诞生,JS 进入服务端,急需模块系统,CommonJS 应运而生。
2)核心语法(同步)
js
// 导出(module.exports / exports)
// math.js
exports.add = (a, b) => a + b;
module.exports = { add };
// 导入(require)
const math = require('./math');
console.log(math.add(1, 2));
3)核心特点
- 每个文件就是一个模块,有独立作用域。
- 运行时解析、同步加载 :
require是函数,运行时才去读文件、执行。 - 缓存机制 :第一次加载后缓存,后续
require直接拿缓存。 - 循环依赖:部分加载
- A 依赖 B,B 依赖 A
- CommonJS 会先导出空对象 ,继续执行,最后补全,可能拿到不完整的对象。
4)适用场景 & 局限
- ✅ 适合服务器端(Node.js):本地文件、IO 快、同步没问题。
- ❌ 不适合浏览器:同步加载会阻塞页面,不能跨域直接用。
三、AMD:浏览器异步模块(2011,RequireJS)
1)背景
浏览器不能同步阻塞,所以 AMD(Asynchronous Module Definition)主打异步、并行加载。
2)核心语法(异步)
js
// 定义模块:define(依赖数组, 工厂函数)
// math.js
define([], function() {
return {
add(a, b) { return a + b; }
};
});
// 使用模块
define(['./math'], function(math) {
console.log(math.add(1, 2));
});
3)特点
- 异步并行加载:不阻塞页面,依赖并行下载。
- 依赖前置、提前声明:依赖写在数组里,加载器先加载所有依赖再执行回调。
- 浏览器优先:RequireJS 是代表。
4)缺点
- 语法繁琐 :
define + 依赖数组 + 回调。 - 服务端不友好:Node.js 原生不支持,需兼容层。
四、CMD:国内 Sea.js(2012,阿里)
1)定位
CMD(Common Module Definition)是 Sea.js 提的,借鉴 CommonJS 写法、浏览器异步。
2)语法(就近依赖)
js
// CMD
define(function(require, exports, module) {
// 依赖就近写
const math = require('./math');
exports.add = math.add;
});
3)对比 AMD
- AMD:依赖前置(先加载所有依赖)
- CMD:依赖就近(用到再 require)
现在基本被 ESM + 打包工具取代。
五、UMD:通用模块(2014,兼容 CommonJS/AMD)
1)目的
写一个模块,同时支持 CommonJS、AMD、全局变量,适配各种环境。
2)典型模板
js
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define([], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS
module.exports = factory();
} else {
// 全局
root.myModule = factory();
}
})(typeof self !== 'undefined' ? self : this, function() {
return { add(a,b) {return a+b;} };
});
3)现状
- 老库常用 UMD,新项目直接 ESM。
六、ES Module(ESM):语言原生标准(2015,ES6/ES2015)
1)里程碑
第一次把模块化纳入 JS 语言标准,浏览器和 Node.js 原生支持。
2)语法(静态 import/export)
js
// 导出
// math.js
export const add = (a, b) => a + b;
export default { add };
// 导入
import { add } from './math.js';
import math from './math.js';
3)核心特性(对比 CommonJS)
① 静态分析(Tree-Shaking)
import必须在模块顶部,编译阶段就能确定依赖。- 打包工具可删除未使用代码(Tree-Shaking),体积更小。
- CommonJS 是运行时
require,无法静态分析、不能 Tree-Shaking。
② 异步加载 + 动态导入
- 浏览器中
<script type="module">默认异步、不阻塞。 - 动态导入:
import()函数,按需加载、懒加载。
js
// 动态导入(运行时)
import('./math.js').then(math => {
console.log(math.add(1,2));
});
③ 循环依赖处理更优
- ESM 用绑定(binding)而非拷贝,循环依赖时拿到实时绑定,不是快照。
④ 跨平台统一
- 浏览器:
<script type="module" src="xxx.js">。 - Node.js:
.mjs或package.json加"type": "module"。
4)ESM vs CommonJS 关键对比
| 特性 | CommonJS | ES Module(ESM) |
|---|---|---|
| 加载时机 | 运行时解析、同步加载 | 编译时静态分析、异步加载 |
| 语法 | require / module.exports | import / export(静态) |
| Tree-Shaking | ❌ 不支持 | ✅ 支持(静态导入) |
| 循环依赖 | 部分加载(空对象) | 实时绑定,更可靠 |
| 浏览器原生 | ❌ 不支持 | ✅ type="module" |
| Node.js | ✅ 默认支持 | ✅ .mjs / "type":"module" |
七、工程化工具:抹平差异(2012 至今)
1)Browserify(2012)
- 让浏览器能用 CommonJS,打包成一个文件。
- 原理:把
require转成浏览器可执行的函数。
2)Webpack(2014 崛起)
- 支持 CommonJS + AMD + ESM,统一打包。
- 核心:依赖图(Dependency Graph),从入口递归找依赖,打包输出。
- 现在主流:源码写 ESM,Webpack/Rollup/Vite 打包。
3)Vite(2020)
- 浏览器原生 ESM + 按需编译,开发环境极速冷启动。
八、总结:模块化演进脉络(一句话)
- 1995--2009:无模块 → 全局变量 → IIFE(私有作用域,无依赖管理)。
- 2009:CommonJS(Node.js,同步,运行时)。
- 2011:AMD(RequireJS,浏览器,异步)。
- 2014:UMD(兼容 CommonJS/AMD)。
- 2015:ESM(ES6,语言标准,静态+异步,跨平台)。
- 2012--now:Browserify → Webpack → Vite(工具抹平差异,统一 ESM)。
本质演进方向 :
全局 → 私有作用域 → 依赖管理 → 异步加载 → 语言原生标准 → 工程化统一。