1. 模块化的前世今生
2009年我刚接触前端开发时,项目里的JavaScript文件总是乱成一锅粥。全局变量满天飞,函数命名得像绕口令,项目超过3000行代码就开始原地爆炸。直到模块化概念出现,就像给混乱的代码世界带来了收纳整理师。
现代模块化的核心目标是实现代码隔离、依赖管理和按需加载。就像把乐高积木分装在不同盒子里,需要时取出组装。目前主流的四种方案各有特色,我们通过代码示例来感受它们的差异。
2. ES Modules详解
2.1 现代浏览器的原生支持
// utils.mjs (技术栈:现代浏览器/Node.js 14+)
export const formatPrice = (num) => `¥${num.toFixed(2)}`;
export default class Calculator {
static add(a, b) {
return a + b * (this.taxRate || 1);
}
static setTaxRate(rate) {
this.taxRate = 1 + rate;
}
}
// main.mjs
import Calc, { formatPrice } from './utils.mjs';
Calc.setTaxRate(0.13); // 设置13%税率
console.log(formatPrice(Calc.add(100, 50))); // ¥169.50
2.2 运行原理解密
- 静态分析:打包工具可以通过
import
/export
语句进行tree-shaking - 实时绑定:导出的变量是引用而不是拷贝,这和我们熟悉的CommonJS不同
- 严格模式:模块自动启用严格模式,避免意外的全局变量
3. CommonJS:Node.js的模块化初心
3.1 服务器端的同步哲学
// db.js (技术栈:Node.js)
const mysql = require('mysql');
const pool = mysql.createPool({
connectionLimit: 10,
host: 'localhost'
});
module.exports.query = (sql) => {
return new Promise((resolve, reject) => {
pool.query(sql, (err, results) => {
err ? reject(err) : resolve(results);
});
});
};
// app.js
const { query } = require('./db');
async function getUser() {
try {
const users = await query('SELECT * FROM users LIMIT 1');
return users[0];
} catch (err) {
console.error('数据库炸了:', err);
throw err;
}
}
3.2 有趣的循环依赖问题
当A模块加载B,而B又反过来加载A时,CommonJS的处理方式往往让新手困惑。这种情况下,B拿到的A模块是未完成初始化的中间状态。
4. AMD:浏览器时代的异步先驱
4.1 RequireJS实战演示
// 配置require.config.js
require.config({
baseUrl: '/src',
paths: {
'jquery': 'lib/jquery-3.6.0.min'
}
});
// math.js
define(['jquery'], function($) {
// 展示页面加载动画
$('#loading').fadeIn();
return {
sum: function(arr) {
return arr.reduce((a, b) => a + b, 0);
}
};
});
// main.js
require(['math'], function(math) {
console.log(math.sum([1, 2, 3])); // 6
$('#loading').fadeOut();
});
4.2 依赖前置的利与弊
AMD的强制依赖声明虽然保证了执行顺序,但在开发大型应用时,配置文件的维护会成为技术债的重灾区。曾经在一个电商项目中,我们的require.config.js文件长达800多行,每次添加新模块都要胆战心惊。
5. UMD:通用模块的万能胶
5.1 实现跨环境兼容
// logger.umd.js
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined'
? module.exports = factory()
: typeof define === 'function' && define.amd
? define(factory)
: (global.logger = factory());
})(this, function() {
let logLevel = 'info';
return {
debug: function(msg) {
if (logLevel === 'debug') console.debug(`[DEBUG] ${msg}`);
},
setLevel: function(level) {
logLevel = level;
}
};
});
// 浏览器使用
logger.setLevel('debug');
logger.debug('测试信息');
// CommonJS使用
const logger = require('./logger.umd');
logger.debug('服务器日志');
// AMD环境
define(['logger.umd'], function(logger) {
logger.debug('AMD模块调用');
});
5.2 实际项目中的妥协方案
在给开源库打包时,UMD几乎是标配。但要注意打包体积会增大20%-30%,建议配合代码压缩工具使用。
6. 四大方案对比擂台
6.1 特性对照表
特性 | ES Modules | CommonJS | AMD | UMD |
---|---|---|---|---|
加载方式 | 静态 | 同步 | 异步 | 兼容 |
浏览器支持 | 原生 | 不直接 | 需要加载器 | 需要加载器 |
Node.js支持 | .mjs扩展名 | 原生 | 需转换 | 需转换 |
Tree-shaking | 支持 | 有限 | 不支持 | 不支持 |
动态导入 | import() | require | require | 视情况 |
6.2 选择困难症指南
- 企业级后台系统:推荐ES Modules + Webpack,利用代码拆分优化首屏加载
- Node.js服务端:CommonJS仍然是主流选择
- 需要兼容IE11的老项目:AMD + RequireJS仍是可靠选项
- 开源工具库开发:UMD确保最大兼容性
7. 踩坑合集与最佳实践
- ES Modules的.mjs陷阱:在Node.js中使用时,建议在package.json中明确设置
"type": "module"
- AMD配置地狱:采用分模块配置,避免单个配置文件过大
- 循环依赖处理:尽量通过代码重构避免,必要时用工厂函数延迟加载
- Webpack的魔法注释:
/* webpackChunkName: "lodash" */
实现按需加载
8. 未来趋势展望
随着Vite、Snowpack等基于ESM的构建工具崛起,模块化方案的界限正在模糊。Deno的横空出世更是直接采用ES Modules作为默认标准,Node.js也在逐步实现ESM和CJS的无缝互操作。