一、为什么需要模块化?
在早期前端开发中,所有 JS 都写在一个文件里,或者用多个
-
全局变量污染:不同文件里的变量可能重名,覆盖导致 bug。
-
依赖管理混乱:多个文件加载顺序不对就会报错(比如先用 jQuery 后加载 jQuery 源码)。
-
维护困难:项目大了,功能代码难以分层,复用性差。
模块化就是为了解决这些问题,把代码拆分成 独立的模块(独立作用域、职责单一),按需引入使用。
二、模块化发展历程
- 立即执行函数(IIFE)阶段
- 不是规范,只是一种 代码写法(非官方)
定义 moduleA.js
javascript
// moduleA.js
var moduleA = (function () {
var count = 0;
function add() {
return ++count;
}
return { add };
})();
在 HTML 引入
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>IIFE 模块示例</title>
</head>
<body>
<div id="app"></div>
<!-- 先引入 moduleA -->
<script src="moduleA.js"></script>
<!-- 再写业务代码 -->
<script>
console.log(moduleA.add()); // 1
console.log(moduleA.add()); // 2
</script>
</body>
</html>
优点:避免污染全局作用域。
缺点:依赖、模块间关系不好管理。
- CommonJS(Node.js 推广的规范)
- 2009 年 Node.js 社区提出的规范,不是 ECMAScript 官方标准。
- 使用 require 导入,module.exports 导出。
- 使用函数实现。
- 仅node环境支持。
- 动态依赖。
- 动态依赖是同步执行的。
javascript
// math.js
function add(a, b) {
return a + b;
}
module.exports = { add };
// main.js
const { add } = require('./math');
console.log(add(2, 3)); // 5
- AMD(Asynchronous Module Definition)
- define:定义模块(依赖哪些模块,返回什么对象)。
- require:加载模块并执行。
- 需要 引入 RequireJS 才能让 AMD 规范跑起来,否则浏览器不认识 define / require。
- 特点:异步加载模块,适合浏览器。
- 写法繁琐,社区用过一阵子(社区规范,非官方),但现在基本淘汰。
定义模块 moduleA.js
javascript
// moduleA.js
define(['jquery'], function ($) {
return {
show: () => $('#app').text('hello AMD')
}
});
在 HTML 里引入 RequireJS 和模块
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>AMD 示例</title>
<!-- 引入 RequireJS,data-main 表示入口文件 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"
data-main="main"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
编写入口文件 main.js
javascript
// main.js
require(['./moduleA'], function (moduleA) {
moduleA.show();
});
- CMD(Common Module Definition)
- 特点:依赖就近、按需加载。
- 由国内的 SeaJS 团队提出。
- 社区规范,非官方。
- define:定义一个 CMD 模块。
- require:在函数内部按需加载依赖(CMD 特点:依赖就近)。
- exports / module.exports:对外暴露 API。
- seajs.use:入口调用,类似 AMD 的 require([...], callback)。
.定义模块 moduleA.js
javascript
// moduleA.js
define(function(require, exports, module) {
var $ = require('jquery');
exports.show = () => $('#app').text('hello CMD');
});
在 HTML 里引入 SeaJS
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>CMD 示例</title>
<!-- 引入 SeaJS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/seajs/3.0.0/sea.js"></script>
</head>
<body>
<div id="app"></div>
<script>
// 使用 seajs.use 加载入口模块
seajs.use('./moduleA.js', function(moduleA) {
moduleA.show();
});
</script>
</body>
</html>
区别对比:
-
AMD (RequireJS):依赖前置,提前声明。
-
CMD (SeaJS):依赖就近,用到时才 require。
- ES Module(ESM,现代前端的主流方案)
- 静态依赖分析(编译时就能确定依赖关系),浏览器和 Node.js 都原生支持。
- ECMAScript 官方标准,是现代前端的 唯一官方模块化方案。
- 动态依赖是异步的。
- 符号绑定。
javascript
// math.js
export function add(a, b) {
return a + b;
}
// main.js
import { add } from './math.js';
console.log(add(2, 3)); // 5
关于符号绑定:
在 ES Module 里,import 导入的不是值的拷贝,而是和原始模块变量的"绑定"(引用关系)。
-
这个绑定是 只读的(不能在 import 模块里重新赋值)。
-
但是如果原始模块里变量的值发生变化,import 进来的地方能感知到更新。
javascript
//a.js
export let count = 0;
export function add() {
count++;
}
//b.js
import { count, add } from './a.js';
console.log(count); // 0
add();
console.log(count); // 1 ✅ 自动更新,因为是绑定
在这里,count 在 a.js 里变了,b.js 里看到的 count 也跟着变。
三、模块化的好处
-
作用域隔离:避免全局污染。
-
按需加载:提升性能。
-
依赖清晰:结构更清楚,便于维护。
-
复用性强:不同项目可复用模块。
四、现代前端中的模块化工具
即使有了 ES Module,实际项目中还是需要 打包工具:
-
Webpack:支持 CommonJS、ESM、AMD,适合大型项目。
-
Rollup:更适合库的打包,Tree-shaking 优秀。
-
Vite / ESBuild:基于 ESM 的新一代构建工具,速度快。