JavaScript 模块化演进史:从 CommonJS 到 ES Modules
引言:模块化的必要性
在 JavaScript 早期发展阶段,语言本身并没有内置的模块系统。这导致开发者在构建大型应用时面临着诸多挑战:
全局命名空间污染
// script1.js
var utils = {
formatDate: function(date) { /* ... */ }
};
// script2.js - 意外覆盖了同名变量
var utils = {
formatDate: function(date) { /* 不同实现 */ }
};
// script3.js - 使用时出现意外行为
utils.formatDate(new Date()); // 调用的是哪个实现?
这种命名冲突在引入多个第三方库时尤为严重。著名的例子是早期的 Prototype.js 与 jQuery 之间的 $ 符号冲突。
依赖管理混乱
在模块化之前,开发者必须通过 HTML <script> 标签手动管理加载顺序:
<!-- 必须严格按照依赖顺序加载 -->
<script src="jquery.js"></script>
<script src="underscore.js"></script>
<script src="backbone.js"></script>
<script src="app.js"></script>
<!-- 如果顺序错误,就会报错 -->
<script src="app.js"></script> <!-- Uncaught Error: Backbone is not defined -->
<script src="backbone.js"></script>
作用域问题
// 所有变量都在全局作用域
var dbPassword = "secret123"; // 任何脚本都能访问
// 通过 IIFE 创建私有作用域(当时的解决方案)
var MyModule = (function() {
var privateVar = "private";
return {
getPrivate: function() {
return privateVar;
}
};
})();
虽然 IIFE(立即执行函数表达式)提供了一定的封装能力,但这种方式无法解决模块间的依赖管理问题。
可维护性挑战
随着项目规模的增长,缺乏模块化导致:
- 代码定位困难:无法快速找到功能定义位置
- 重构风险高:修改一处可能影响多处
- 测试困难:无法独立测试单个模块
- 团队协作效率低:多人开发时容易产生冲突
CommonJS (CJS) - 服务端模块化的先驱
时间: 2009年1月
提出者: Kevin Dangoor(ServerJS 项目,后更名为 CommonJS)
背景: CommonJS 的诞生源于 JavaScript 在服务器端应用的需求。当时 Node.js 刚刚发布(2009年),Ryan Dahl 选择了 CommonJS 规范作为 Node.js 的模块系统基础。
历史背景
2009年之前,JavaScript 主要用于浏览器端的小脚本。当 Node.js 出现后,JavaScript 开始进入服务器开发领域,但面临一个核心问题:如何组织和管理服务器端的 JavaScript 代码?
CommonJS 项目的目标是创建一个标准的 JavaScript 库生态系统,让 JavaScript 代码能够在不同 JavaScript 运行时之间共享。
核心规范
CommonJS Modules/1.0 规范定义了三个核心概念:
- require() - 导入模块的函数
- exports - 导出模块内容的对象
- module - 表示当前模块的对象
详细语法
// ===== 基本导出 =====
// 方式1:直接赋值给 module.exports
module.exports = function Calculator() {
this.add = function(a, b) {
return a + b;
};
};
// 方式2:使用 exports 添加属性(注意:不能直接赋值)
exports.PI = 3.14159;
exports.calculateArea = function(radius) {
return this.PI * radius * radius;
};
// 错误示例:直接赋值会断开引用
exports = function() {}; // ❌ 无效
// ===== 基本导入 =====
// 导入核心模块
const fs = require('fs');
const path = require('path');
// 导入文件模块(相对路径必须以 ./ 或 ../ 开头)
const myUtils = require('./utils');
const parentModule = require('../parent');
// 导入 node_modules 中的模块
const lodash = require('lodash');
const express = require('express');
// ===== 导入时的解构 =====
// utils.js
exports.add = function(a, b) { return a + b; };
exports.subtract = function(a, b) { return a - b; };
// app.js
const { add, subtract } = require('./utils');
console.log(add(5, 3)); // 8
// ===== 模块缓存机制 =====
// 第一次 require 会执行并缓存
const mod1 = require('./myModule'); // 执行模块代码
const mod2 = require('./myModule'); // 从缓存返回
console.log(mod1 === mod2); // true - 同一个实例
模块加载流程
// Node.js 的模块加载流程(简化版)
function require(modulePath) {
// 1. 解析模块路径
const resolvedPath = Module._resolveFilename(modulePath);
// 2. 检查缓存
if (Module._cache[resolvedPath]) {
return Module._cache[resolvedPath].exports;
}
// 3. 创建新模块
const module = new Module(resolvedPath);
// 4. 缓存模块
Module._cache[resolvedPath] = module;
// 5. 加载模块文件
module.load(resolvedPath);
// 6. 返回导出内容
return module.exports;
}
模块解析算法
Node.js 按照以下顺序查找模块:
// require('./myModule') 的查找顺序
// 1. 精确文件名
./myModule.js
./myModule.json
./myModule.node
// 2. 目录形式
./myModule/package.json (查找 main 字段)
./myModule/index.js
./myModule/index.json
./myModule/index.node
// 3. node_modules 查找(向上递归)
./node_modules/myModule
../node_modules/myModule
../../node_modules/myModule
// ... 直到根目录
循环依赖处理
CommonJS 通过值拷贝和执行顺序处理循环依赖:
// a.js
console.log('a 开始');
exports.loaded = false;
const b = require('./b');
console.log('a 中,b.loaded =', b.loaded);
exports.loaded = true;
console.log('a 结束');
// b.js
console.log('b 开始');
exports.loaded = false;
const a = require('./a');
console.log('b 中,a.loaded =', a.loaded);
exports.loaded = true;
console.log('b 结束');
// main.js
const a = require('./a');
const b = require('./b');
// 输出:
// a 开始
// b 开始
// b 中,a.loaded = false ← a 还未完全加载
// b 结束
// a 中,b.loaded = true ← b 已完全加载
// a 结束
同步加载的原理
// 文件系统读取是同步的
const fs = require('fs');
const content = fs.readFileSync('config.json', 'utf8');
// 在服务器端,I/O 操作虽然快,但仍然是同步阻塞的
// 这在浏览器中会导致界面卡死
const module = require('./heavy-computation'); // 阻塞直到加载完成
优缺点分析
优点:
- 简洁直观:语法简单,易于理解
- 服务端性能好:本地文件系统访问速度快
- 成熟的生态系统:npm 基于 CommonJS
- 明确的依赖关系:通过 require() 清晰表达依赖
缺点:
- 同步加载不适合浏览器:网络请求会导致阻塞
- 不支持静态分析:无法在编译时确定依赖
- 无法 Tree-shaking:打包工具难以消除未使用代码
- 运行时加载:性能低于静态加载
实际应用示例
// config/config.js - 配置模块
module.exports = {
database: {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
name: process.env.DB_NAME || 'myapp'
},
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379
}
};
// services/database.js - 数据库服务
const config = require('../config');
class Database {
constructor() {
this.connection = null;
}
connect() {
console.log(`Connecting to ${config.database.host}:${config.database.port}`);
// 连接逻辑
}
}
module.exports = new Database();
// app.js - 应用入口
const db = require('./services/database');
const http = require('http');
db.connect();
const server = http.createServer((req, res) => {
res.end('Hello World');
});
server.listen(3000);
AMD (Asynchronous Module Definition) - 浏览器端的探索
时间: 2011年
提出者: James Burke(RequireJS 作者)
背景: AMD 规范的出现直接针对浏览器环境的特殊性。在浏览器中,模块需要通过网络加载,同步加载会导致页面阻塞和糟糕的用户体验。
历史背景
2010年前后,Web 应用开始变得复杂,需要加载大量 JavaScript 文件。传统的 <script> 标签方式存在以下问题:
- 阻塞渲染:脚本执行期间页面无法响应用户交互
- 加载顺序依赖:必须手动管理复杂的依赖关系
- 性能问题:无法并行加载和执行
- 难以调试:全局命名空间混乱
James Burke 在开发 Dojo Toolkit 时遇到了这些问题,于是创建了 RequireJS 和 AMD 规范。
核心规范
AMD 规范定义了一个简单的 API:
define(id?, dependencies?, factory);
- id(可选):模块标识符
- dependencies(可选):依赖模块数组
- factory:模块工厂函数
详细语法
// ===== 基本模块定义 =====
// 无依赖的模块
define(function() {
return {
getVersion: function() {
return '1.0.0';
}
};
});
// 有依赖的模块
define(['jquery', 'underscore'], function($, _) {
function processData(data) {
// 使用依赖
var sorted = _.sortBy(data, 'id');
return $('#result').html(sorted);
}
return {
process: processData
};
});
// 具名模块(不推荐,仅用于特殊情况)
define 'app/utils', ['lodash'], function(_) {
return {
clone: _.clone
};
});
// ===== 模块导入 =====
// 全局 require
require(['myModule', 'anotherModule'], function(myMod, anotherMod) {
myMod.doSomething();
anotherMod.doOtherThing();
});
// 局部 require(在模块内动态加载)
define(function(require) {
var button = document.getElementById('loadButton');
button.addEventListener('click', function() {
// 点击时才加载模块
require(['heavyModule'], function(heavy) {
heavy.process();
});
});
});
// ===== CommonJS 兼容写法 =====
// AMD 支持类似 CommonJS 的写法(通过简化 CommonJS 包装)
define(function(require, exports, module) {
var dep = require('dependency');
exports.value = dep.transform();
});
RequireJS 配置
// main.js - RequireJS 入口配置
requirejs.config({
// 基础路径,所有模块查找的起点
baseUrl: './js',
// 路径映射(简化模块名)
paths: {
'jquery': 'libs/jquery-2.1.4.min',
'underscore': 'libs/underscore-min',
'backbone': 'libs/backbone-min'
},
// 模块配置(用于非 AMD 模块)
shim: {
'underscore': {
exports: '_'
},
'backbone': {
deps: ['jquery', 'underscore'],
exports: 'Backbone'
},
'jqueryScroll': {
deps: ['jquery'],
exports: 'jQuery.fn.scroll'
}
},
// 等待超时时间
waitSeconds: 15,
// 调试模式
enforceDefine: true,
// URL 参数(避免缓存)
urlArgs: "bust=" + (new Date()).getTime()
});
// 启动应用
require(['app/main'], function(App) {
App.initialize();
});
异步加载机制
// AMD 使用回调处理异步加载
define(['module1', 'module2', 'module3'], function(mod1, mod2, mod3) {
// 三个模块并行加载,全部完成后执行回调
// 即使 module3 加载最快,也会等待其他模块
mod1.init();
mod2.setup();
mod3.run();
});
// 加载时序
// [并行开始]
// ├── module1.js ────────┐
// ├── module2.js ────┐ │
// └── module3.js ─┐ │ │
// ↓ ↓ ↓
// [全部完成] → 执行回调
依赖前置 vs 就近依赖
// AMD:依赖前置(所有依赖在开头声明)
define(['jquery', './utils', './config'], function($, utils, config) {
function initFeatureA() {
// jQuery 在这里使用
$('#featureA').show();
}
function initFeatureB() {
// utils 在这里使用
utils.process();
}
// config 在这里使用
var api = config.apiUrl;
return {
initA: initFeatureA,
initB: initFeatureB
};
});
优缺点分析
优点:
- 异步加载:不阻塞页面渲染,用户体验好
- 并行加载:多个模块可以同时下载
- 成熟的实现:RequireJS 功能完善,文档齐全
- 循环依赖检测:可以检测并处理循环依赖
- 插件支持:支持文本、CSS 等资源的加载
缺点:
- 语法复杂:define 回调嵌套,代码可读性差
- 依赖前置:可能加载不需要的模块
- 开发体验不佳:配置相对复杂
- 调试困难:多层回调增加调试难度
实际应用示例
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
<!-- 引入 RequireJS -->
<script data-main="js/main" src="js/require.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
// js/main.js
requirejs.config({
paths: {
jquery: 'lib/jquery.min',
underscore: 'lib/underscore.min',
backbone: 'lib/backbone.min'
},
shim: {
underscore: {
exports: '_'
},
backbone: {
deps: ['jquery', 'underscore'],
exports: 'Backbone'
}
}
});
require(['app'], function(App) {
App.start();
});
// js/app.js
define(['jquery', 'backbone', 'views/appView'], function($, Backbone, AppView) {
var App = {
start: function() {
new AppView({el: $('#app')});
Backbone.history.start();
}
};
return App;
});
// js/views/appView.js
define(['backbone', 'collections/todos'], function(Backbone, TodoList) {
var AppView = Backbone.View.extend({
initialize: function() {
this.todos = new TodoList();
this.todos.fetch();
this.render();
},
render: function() {
this.$el.html('<h1>Todo App</h1>');
return this;
}
});
return AppView;
});
CMD (Common Module Definition) - 国内探索
时间: 2012年
提出者: 玉伯(淘宝前端架构师)
背景: CMD 是在中国互联网快速发展的背景下诞生的。当时国内开发者在使用 AMD 规范时遇到一些问题,玉伯在开发 SeaJS 时提出了 CMD 规范。
历史背景
2011-2012年,中国互联网迎来爆发式增长,淘宝等大型电商网站的前端复杂度急剧提升。国内开发者在实践中发现:
- AMD 的依赖前置在某些场景下不够灵活
- 开发体验与 CommonJS 差异较大
- 需要一个更符合国内开发习惯的规范
玉伯受到 CommonJS 的启发,设计了 CMD 规范,强调”依赖就近”和”延迟执行”。
核心规范
CMD 规范的特点:
define(function(require, exports, module) {
// 模块代码
});
- require:导入函数(同步)
- exports:导出对象
- module:模块对象
详细语法
// ===== 基本模块定义 =====
// 简单模块
define(function(require, exports, module) {
var $ = require('jquery');
function init() {
$('#app').show();
}
exports.init = init;
});
// ===== 依赖就近(核心特性)=====
define(function(require, exports, module) {
// 依赖可以就近书写
function featureA() {
// 在需要时才引入依赖
var moduleA = require('./moduleA');
moduleA.run();
}
function featureB() {
// 延迟加载,使用时才引入
var moduleB = require('./moduleB');
moduleB.process();
}
exports.featureA = featureA;
exports.featureB = featureB;
});
与 AMD 的核心区别
// ===== AMD:依赖前置 =====
define(['jquery', './utils', './config'], function($, utils, config) {
// 所有依赖在开头声明并加载
// 即使某些依赖在后面才用到
function init() {
$('#app').show();
}
function process() {
// utils 和 config 在这里才使用
// 但在模块加载时就已经被加载了
utils.format(config.data);
}
return { init: init, process: process };
});
// ===== CMD:依赖就近 =====
define(function(require, exports, module) {
function init() {
// 需要时才引入
var $ = require('jquery');
$('#app').show();
}
function process() {
// 使用时才引入
var utils = require('./utils');
var config = require('./config');
utils.format(config.data);
}
exports.init = init;
exports.process = process;
});
延迟执行机制
// AMD:立即执行
define(['moduleA', 'moduleB'], function(A, B) {
// A 和 B 的模块代码已经执行
console.log(A.initialized); // true
console.log(B.initialized); // true
});
// CMD:延迟执行
define(function(require, exports, module) {
// 此时模块A和模块B还没有加载
function later() {
var A = require('moduleA'); // 此时才加载和执行模块A
console.log(A.initialized); // true
var B = require('moduleB'); // 此时才加载和执行模块B
}
exports.later = later;
});
SeaJS 配置
// seajs.config
seajs.config({
// 别名配置
alias: {
'jquery': 'jquery/jquery/1.10.1/jquery',
'app': 'src/app'
},
// 路径配置
paths: {
'gallery': 'https://a.alipayobjects.com/gallery'
},
// 变量配置
vars: {
'locale': 'zh-cn'
},
// 映射配置
map: [
['http://example.com/js/app/', 'http://localhost/js/app/']
],
// 预加载项
preload: [
Function.prototype.bind ? '' : 'es5-safe',
'seajs-debug'
],
// 调试模式
debug: true,
// 基础路径
base: 'http://example.com/js/',
// 文件编码
charset: 'utf-8'
});
// 加载主模块
seajs.use('./main');
优缺点分析
优点:
- 依赖就近:更符合直觉,代码可读性好
- 延迟执行:按需加载,性能更好
- 语法简洁:接近 CommonJS,学习成本低
- 国内生态:文档和社区支持好(国内)
缺点:
- 生态局限:主要在国内使用,国际影响力有限
- 工具支持:相比 AMD,工具链支持较少
- 逐步淘汰:随着 ESM 的普及,使用减少
实际应用示例
// modules/dialog.js - 对话框模块
define(function(require, exports, module) {
var $ = require('jquery');
var template = require('./dialog-template');
function Dialog(options) {
this.options = options;
this.el = $(template);
}
Dialog.prototype.show = function() {
var self = this;
// 延迟加载动画库
require('./animate', function(animate) {
self.el.appendTo('body');
animate.fadeIn(self.el);
});
};
Dialog.prototype.hide = function() {
this.el.remove();
};
module.exports = Dialog;
});
// modules/app.js - 应用模块
define(function(require, exports, module) {
var $ = require('jquery');
var Dialog = require('./dialog');
function init() {
$('#showDialog').on('click', function() {
var dialog = new Dialog({
title: '提示',
content: 'Hello CMD!'
});
dialog.show();
});
}
exports.init = init;
});
// main.js - 入口文件
define(function(require) {
var app = require('./modules/app');
$(document).ready(function() {
app.init();
});
});
AMD vs CMD 对比表
| 特性 | AMD | CMD |
|---|---|---|
| 依赖声明 | 前置(定义时) | 就近(使用时) |
| 执行时机 | 加载后立即执行 | 延迟到需要时 |
| API 风格 | 回调函数 | 更接近 CommonJS |
| 代表实现 | RequireJS | SeaJS |
| 主要影响区域 | 国际 | 国内 |
| 学习曲线 | 较陡 | 平缓 |
| 适用场景 | 依赖关系明确的模块 | 复杂条件依赖 |
| 性能特点 | 可能提前加载不需要的模块 | 按需加载,更灵活 |
UMD (Universal Module Definition) - 通用方案
时间: 2014年左右
背景: UMD 不是一种新的模块规范,而是一种模式,让代码能够同时运行在 CommonJS、AMD 和全局变量环境中。
设计理念
UMD 的核心思想是通过运行时环境检测,使用适当的模块系统:
// UMD 的基本模式
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD 环境
define([], factory);
} else if (typeof exports === 'object') {
// CommonJS 环境
module.exports = factory();
} else {
// 浏览器全局变量环境
root.MyLibrary = factory();
}
}(this, function() {
// 模块实现
return {};
}));
详细实现模式
模式1:无依赖的 UMD
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define([], factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = factory();
} else {
// Browser globals
root.MyLibrary = factory();
}
}(typeof self !== 'undefined' ? self : this, function() {
function MyLibrary() {
this.version = '1.0.0';
}
MyLibrary.prototype.doSomething = function() {
console.log('Doing something...');
};
return MyLibrary;
}));
模式2:带依赖的 UMD
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD: 注册为匿名模块
define(['dependency1', 'dependency2'], factory);
} else if (typeof exports === 'object') {
// CommonJS-like
module.exports = factory(require('dependency1'), require('dependency2'));
} else {
// Browser globals
root.MyLibrary = factory(root.Dependency1, root.Dependency2);
}
}(typeof self !== 'undefined' ? self : this, function(dep1, dep2) {
function MyLibrary(options) {
this.dep1 = dep1;
this.dep2 = dep2;
}
MyLibrary.prototype.process = function(data) {
return this.dep1.transform(data) + this.dep2.format(data);
};
return MyLibrary;
}));
模式3:jQuery 插件风格
(function (factory) {
if (typeof define === 'function' && define.amd) {
// AMD: 注册为匿名模块
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = function(root, jQuery) {
if (jQuery === undefined) {
if (typeof window !== 'undefined') {
jQuery = require('jquery');
} else {
jQuery = require('jquery')(root);
}
}
factory(jQuery);
return jQuery;
};
} else {
// Browser globals
factory(jQuery);
}
}(function($) {
$.fn.myPlugin = function(options) {
var settings = $.extend({
color: 'red',
backgroundColor: 'yellow'
}, options);
return this.each(function() {
$(this).css({
color: settings.color,
backgroundColor: settings.backgroundColor
});
});
};
}));
使用工具生成 UMD
手动编写 UMD 包装代码比较繁琐,可以使用工具自动生成:
# 使用 rollup-plugin-commonjs
npm install --save-dev rollup-plugin-commonjs
# rollup.config.js
import commonjs from 'rollup-plugin-commonjs';
export default {
input: 'src/index.js',
output: {
file: 'dist/my-library.js',
format: 'umd',
name: 'MyLibrary'
},
plugins: [
commonjs()
]
};
// webpack 配置
module.exports = {
entry: './src/index.js',
output: {
library: 'MyLibrary',
libraryTarget: 'umd',
filename: 'my-library.js'
}
};
UMD 模板
使用 UMD 模板简化代码:
// 使用 umd 模板
var umd = (function(factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var v = factory(require, exports);
if (v !== undefined) module.exports = v;
} else if (typeof define === "function" && define.amd) {
define(["require", "exports", "./dependency"], factory);
}
})(function(require, exports) {
// 模块代码
exports.value = "something";
});
优缺点分析
优点:
- 兼容性好:一份代码,到处运行
- 适合库作者:无需为不同环境维护多个版本
- 渐进增强:在支持模块系统的环境中使用模块
缺点:
- 代码冗长:包装代码占较大篇幅
- 维护成本高:需要处理多种环境的差异
- 调试困难:多层包装增加调试复杂度
- 逐步淘汰:ESM + 打包工具成为主流
实际应用示例
// utils.js - 纯函数库
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.Utils = factory();
}
}(this, function() {
function Utils() {}
Utils.formatDate = function(date) {
var year = date.getFullYear();
var month = String(date.getMonth() + 1).padStart(2, '0');
var day = String(date.getDate()).padStart(2, '0');
return year + '-' + month + '-' + day;
};
Utils.formatCurrency = function(amount) {
return '$' + amount.toFixed(2);
};
Utils.debounce = function(func, wait) {
var timeout;
return function() {
var context = this, args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function() {
func.apply(context, args);
}, wait);
};
};
return Utils;
}));
// 使用方式
// Node.js (CommonJS)
const Utils = require('./utils');
Utils.formatDate(new Date());
// Browser (RequireJS/AMD)
require(['utils'], function(Utils) {
Utils.formatDate(new Date());
});
// Browser (Script tag global)
<script src="utils.js"></script>
<script>
Utils.formatDate(new Date());
</script>
ES Modules (ESM) - 官方标准
时间: 2015年6月(ES6/ES2015)
提出者: ECMAScript 标准委员会(TC39)
背景: ES Modules 是 JavaScript 官方标准化的模块系统,标志着 JavaScript 模块化进入统一时代。
历史背景
在 ES6 之前,JavaScript 没有官方的模块系统。社区发展出了 CommonJS、AMD、CMD 等多种规范,导致:
- 规范不统一:不同环境使用不同的模块系统
- 互操作困难:CommonJS 模块难以在浏览器直接使用
- 工具链复杂:需要转译工具才能在不同环境使用
TC39 从 2010 年开始着手设计官方模块系统,历经多年讨论,最终在 2015 年随 ES6 一起发布。
核心语法
// ===== 命名导出 =====
// 方式1:导出声明
export const PI = 3.14159;
export let count = 0;
export function calculateArea(radius) {
return PI * radius * radius;
}
export class Calculator {
add(a, b) {
return a + b;
}
}
// 方式2:导出列表
const PI = 3.14159;
function calculateArea(radius) {
return PI * radius * radius;
}
class Calculator {
add(a, b) {
return a + b;
}
}
export { PI, calculateArea, Calculator };
// 方式3:重命名导出
export { calculateArea as area, Calculator as Calc };
// ===== 默认导出 =====
export default function() {
console.log('Default export');
}
// 或
export default class {
constructor() {
this.name = 'Default';
}
}
// 或
const value = 42;
export default value;
// ===== 混合导出 =====
export default function main() {
// 主要功能
}
export const helper1 = () => {};
export const helper2 = () => {};
export const API_KEY = 'abc123';
// ===== 导入 =====
// 命名导入
import { calculateArea, Calculator } from './math.js';
// 重命名导入
import { calculateArea as area } from './math.js';
// 默认导入
import Calculator from './calculator.js';
// 混合导入
import Calculator, { calculateArea as area } from './math.js';
// 导入所有为命名空间
import * as Math from './math.js';
Math.calculateArea(5);
// 副作用导入(只执行,不导入)
import './polyfills.js';
// ===== 动态导入 =====
// ES2020 引入
button.addEventListener('click', async () => {
const module = await import('./heavy-module.js');
module.doSomething();
});
// 条件导入
if (condition) {
const module = await import('./moduleA.js');
module.run();
}
// ===== 重新导出 =====
// 导出再导出(聚合模块)
export { calculateArea, Calculator } from './math.js';
export { default as Utils } from './utils.js';
export * from './helpers.js';
// 默认导出重新导出
export { default } from './main.js';
静态模块结构
ES Modules 在编译时(而非运行时)确定模块依赖关系:
// 编译时分析
import { add } from './math.js'; // ← 静态分析
import { subtract } from './math.js';
// 运行时不能动态导入路径
let moduleName = 'math';
import { multiply } from './' + moduleName; // ❌ 错误
// 必须使用静态字符串字面量
import { multiply } from './math.js'; // ✅ 正确
导入导出详解
导出的是引用
// counter.js
export let count = 0;
export function increment() {
count++;
}
// main.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1 ← 实时更新,不是值拷贝
循环依赖处理
// a.js
import { b } from './b.js';
export const a = 'a';
console.log('a.js:', b);
// b.js
import { a } from './b.js';
export const b = 'b';
console.log('b.js:', a);
// main.js
import './a.js';
// 输出:
// b.js: undefined ← a 还未初始化
// a.js: b
Tree-shaking 支持
ES Modules 的静态结构使得打包工具可以安全地消除未使用的代码:
// utils.js
export function used() {
console.log('Used function');
}
export function unused() {
console.log('This will be removed');
}
export function alsoUnused() {
console.log('This will also be removed');
}
// main.js
import { used } from './utils.js';
used();
// 打包后,unused 和 alsoUnused 会被移除
浏览器原生支持
<!-- index.html -->
<script type="module">
import { add } from './math.js';
console.log(add(5, 3));
</script>
<!-- 动态导入 -->
<script>
button.addEventListener('click', async () => {
const module = await import('./math.js');
console.log(module.add(5, 3));
});
</script>
Node.js 中的 ESM
// 方式1:使用 .mjs 扩展名
// math.mjs
export const add = (a, b) => a + b;
// 方式2:在 package.json 中设置 type
// package.json
{
"type": "module"
}
// 此时 .js 文件被视为 ESM
// math.js
export const add = (a, b) => a + b;
// 导入
import { add } from './math.js';
导入.meta 对象
ES2021 引入了 import.meta,提供模块的元信息:
// 当前模块的 URL
console.log(import.meta.url);
// 使用场景:动态加载相对资源
const response = await fetch(new URL('data.json', import.meta.url));
const data = await response.json();
顶级 await(ES2022)
// config.js
const response = await fetch('./config.json');
export const config = await response.json();
// main.js
import { config } from './config.js';
console.log(config.apiKey);
优缺点分析
优点:
- 官方标准:长期有保障,生态统一
- 语法简洁:比 CommonJS 和 AMD 更优雅
- 静态分析:支持 Tree-shaking,优化打包体积
- 原生支持:现代浏览器和 Node.js 原生支持
- 动态导入:按需加载,性能优化
- 顶级 await:简化异步初始化
缺点:
- 兼容性问题:老版本浏览器需要转译
- 与 CommonJS 互操作:存在一些边界情况
- 学习曲线:对于习惯了 CommonJS 的开发者需要适应
实际应用示例
// lib/math.js
const PI = 3.14159;
export function calculateArea(radius) {
return PI * radius * radius;
}
export function calculateCircumference(radius) {
return 2 * PI * radius;
}
export default {
area: calculateArea,
circumference: calculateCircumference
};
// lib/utils.js
export function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
export function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// app.js
import math, { calculateArea } from './lib/math.js';
import { formatDate, debounce } from './lib/utils.js';
class App {
constructor() {
this.api = 'https://api.example.com';
}
async init() {
console.log('App initialized');
console.log('Area:', calculateArea(5));
console.log('Today:', formatDate(new Date()));
}
setupSearch() {
const search = document.getElementById('search');
const debouncedSearch = debounce(this.performSearch.bind(this), 300);
search.addEventListener('input', debouncedSearch);
}
async performSearch(event) {
const query = event.target.value;
const response = await fetch(`${this.api}/search?q=${query}`);
const results = await response.json();
this.renderResults(results);
}
renderResults(results) {
// 渲染逻辑
}
}
export default App;
// main.js
import App from './app.js';
const app = new App();
app.init();
app.setupSearch();
方案对比总结
详细对比表
| 特性 | CommonJS | AMD | CMD | UMD | ES Modules |
|---|---|---|---|---|---|
| 出现时间 | 2009 | 2011 | 2012 | 2014 | 2015 |
| 主要代表 | Node.js | RequireJS | SeaJS | 多种 | 原生支持 |
| 加载方式 | 同步 | 异步 | 延迟 | 兼容 | 静态/异步 |
| 主要场景 | 服务端 | 浏览器 | 浏览器 | 通用 | 通用 |
| 语法简洁度 | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | ⭐ | ⭐⭐⭐⭐⭐ |
| Tree-shaking | ❌ | ❌ | ❌ | ❌ | ✅ |
| 官方标准 | Node.js规范 | 社区 | 社区 | 社区 | ✅ ES6+ |
| 浏览器支持 | 需转译 | 需RequireJS | 需SeaJS | 需工具 | 原生支持 |
| Node.js支持 | ✅ | 需工具 | 需工具 | 需工具 | ✅ v12+ |
| 静态分析 | ❌ | ⚠️ | ⚠️ | ❌ | ✅ |
| 循环依赖 | 运行时处理 | 支持 | 支持 | 兼容 | 较好支持 |
| 动态导入 | ✅ require() | ✅ require() | ✅ require() | 兼容 | ✅ import() |
| 代码风格 | 简洁 | 回调嵌套 | 简洁 | 包装冗长 | 简洁优雅 |
| 学习曲线 | 平缓 | 中等 | 平缓 | 陡峭 | 平缓 |
| 生态成熟度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
导入导出语法对比
| 操作 | CommonJS | AMD | ES Modules |
|---|---|---|---|
| 导入 | const mod = require('./mod') |
require(['mod'], function(mod){}) |
import mod from './mod.js' |
| 命名导入 | const { fn } = require('./mod') |
需解构 | import { fn } from './mod.js' |
| 导出 | module.exports = {} |
return {} |
export default {} |
| 命名导出 | exports.fn = function(){} |
- | export function fn(){} |
| 动态导入 | const mod = require(path) |
require([path], fn) |
import(path) |
运行时 vs 编译时
// CommonJS - 运行时加载
if (condition) {
const mod = require('./module'); // 可以在条件语句中
}
// AMD - 运行时加载
require(['module'], function(mod) {
if (condition) {
mod.doSomething();
}
});
// ES Modules - 编译时分析
import { mod } from './module.js'; // 必须在顶层
if (condition) {
// 动态导入需要 import()
const mod = await import('./module.js');
}
模块化演进时间线
graph LR
A[2009<br/>CommonJS<br/>Node.js诞生] --> B[2011<br/>AMD规范<br/>RequireJS]
B --> C[2012<br/>CMD规范<br/>SeaJS]
A --> D[2014<br/>UMD模式<br/>通用兼容]
C --> D
D --> E[2015<br/>ES Modules<br/>ES6官方标准]
E --> F[2016-2018<br/>Babel转译时代<br/>向后兼容]
F --> G[2019-2020<br/>Node.js原生ESM<br/>.mjs支持]
G --> H[2021-2022<br/>Vite兴起<br/>现代构建工具]
H --> I[2023-2024<br/>Deno/Bun<br/>新一代运行时]
style A fill:#ff006e,stroke:#d6005c,color:#b3004e
style B fill:#b537f2,stroke:#8a2be2,color:#6a1b9a
style C fill:#00fff5,stroke:#00b8b0,color:#008b82
style E fill:#00ff9f,stroke:#00cc7f,color:#009966
style I fill:#faff00,stroke:#e6d900,color:#c9b800
未来展望(2015年视角)
在 2015 年,ES6 发布带来了 ES Modules 官方标准,这标志着 JavaScript 模块化进入了新纪元:
短期挑战(2015-2017)
- 浏览器兼容性:主流浏览器需要逐步实现 ESM 规范
- 转译需求:Babel 等工具将 ESM 转换为 ES5 以支持老浏览器
- Node.js 支持:需要协调 CommonJS 和 ESM 的关系
中期发展(2017-2020)
- 原生支持普及:现代浏览器普遍支持 ESM
- 工具链成熟:Webpack、Rollup 等打包工具完善 ESM 支持
- 性能优化:Tree-shaking 成为标准优化手段
长期趋势(2020+)
- 统一标准:ESM 成为前后端通用的模块标准
- 工具简化:零配置构建工具(Vite)的兴起
- 新运行时:Deno、Bun 等新一代运行时优先采用 ESM
后续发展(2015年后更新)
2016-2018: Babel 与转译时代
Babel 的出现让开发者可以在生产环境中提前使用 ES Modules:
# 安装 Babel
npm install --save-dev @babel/core @babel/preset-env
# .babelrc
{
"presets": [
["@babel/preset-env", {
"targets": {
"browsers": ["> 1%", "last 2 versions"]
}
}]
]
}
// 源代码
import { add } from './math.js';
// Babel 转译后
'use strict';
var _math = require('./math.js');
// 或转为 UMD 格式
2019-2020: Node.js ESM 原生支持
Node.js 逐步完善了对 ES Modules 的支持:
.mjs 和 .cjs 扩展名
// math.mjs - ES Module
export const add = (a, b) => a + b;
// math.cjs - CommonJS
const add = (a, b) => a + b;
module.exports = { add };
package.json 的 type 字段
{
"name": "my-project",
"version": "1.0.0",
"type": "module",
"exports": {
".": "./src/index.js",
"./legacy": "./dist/legacy.cjs"
}
}
条件导出
{
"exports": {
"import": "./index.mjs",
"require": "./index.cjs",
"default": "./index.js"
}
}
2021-2022: 打包工具演进
Vite 的出现
Vite 利用浏览器原生 ESM 支持,实现了极快的开发体验:
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
build: {
lib: {
entry: 'src/index.js',
name: 'MyLib',
formats: ['es', 'cjs', 'umd']
}
},
optimizeDeps: {
include: ['lodash-es']
}
});
现代打包配置
// rollup.config.js
export default {
input: 'src/index.js',
output: [
{
file: 'dist/bundle.esm.js',
format: 'es'
},
{
file: 'dist/bundle.cjs.js',
format: 'cjs'
},
{
file: 'dist/bundle.umd.js',
format: 'umd',
name: 'MyLibrary'
}
]
};
2023-2024: 新一代方案
Deno - 默认 ESM
// main.ts
import { serve } from "https://deno.land/[email protected]/http/server.ts";
async function handler(req: Request): Promise<Response> {
return new Response("Hello Deno!");
}
await serve(handler, { port: 8000 });
Bun - 高性能运行时
// Bun 优先使用 ESM
import { serve } from 'bun';
Bun.serve({
port: 3000,
fetch(req) {
return new Response("Hello Bun!");
},
});
TypeScript 类型集成
// math.ts
export interface Point {
x: number;
y: number;
}
export function distance(p1: Point, p2: Point): number {
return Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2);
}
// main.ts
import { distance, Point } from './math.js';
const p1: Point = { x: 0, y: 0 };
const p2: Point = { x: 3, y: 4 };
console.log(distance(p1, p2)); // 5
模块化最佳实践(2024)
- 新项目使用 ESM:除非有特殊需求,否则优先使用 ES Modules
- 库的双重发布:同时发布 ESM 和 CJS 版本
- package.json exports:使用条件导出优化兼容性
- Tree-shaking 友好:避免 sideEffects,导出纯函数
- 完整类型支持:使用 TypeScript 或 JSDoc
总结
JavaScript 模块化经历了从社区探索到官方标准化的完整演进过程:
发展脉络
- CommonJS(2009):为服务器端 JavaScript 带来模块化能力,npm 生态的基石
- AMD(2011):解决浏览器异步加载问题,RequireJS 成为主流
- CMD(2012):国内探索,提供更灵活的依赖就近方式
- UMD(2014):跨环境兼容方案,支持库的统一发布
- ES Modules(2015):官方标准,统一前后端模块系统
技术演进
graph TD
A[无模块时代<br/>全局变量污染] --> B[CommonJS<br/>服务端同步]
A --> C[AMD<br/>浏览器异步]
B --> D[UMD<br/>跨环境兼容]
C --> D
D --> E[ES Modules<br/>官方统一标准]
style A fill:#ff006e,stroke:#d6005c,color:#b3004e
style E fill:#00ff9f,stroke:#00cc7f,color:#009966
选择建议(2024)
| 场景 | 推荐方案 |
|---|---|
| 新浏览器项目 | ES Modules + Vite |
| 新 Node.js 项目 | ES Modules(type: “module”) |
| 库开发 | ESM + CJS 双重发布 |
| 遗留项目 | 保持现有方案,逐步迁移 |
| Deno/Bun 项目 | ES Modules |
JavaScript 模块化的演进史,是一部前端工程化发展的缩影。从混乱到有序,从社区探索到官方标准,JavaScript 终于拥有了原生、高效、优雅的模块系统。
参考资源: