JS 五大模块标准完整详解:CommonJS / AMD / CMD / UMD / ESM
一、模块化出现背景
早期 JavaScript 不存在原生模块化方案,开发中存在三大痛点:
- 全局变量污染,变量、函数命名冲突;
- 依赖加载顺序无法自动管理,手动引入脚本极易出错;
- 代码复用、拆分、维护成本极高。
- 社区先后诞生多套模块化规范,最终 ES6 推出官方原生 ESM 统一标准。
五大规范可分为三类:
- 服务端早期规范:CommonJS、UMD
- 浏览器异步模块化:AMD、CMD
- 现代官方原生标准:ESM(ES6 Module)
二、CommonJS(简称 CJS)
通过exports和module.exports来暴露模块中的内容。通过require来加载模块。
-
诞生背景
2009 年随 Node.js 诞生,是 Node.js 内置默认模块规范,面向服务端本地文件设计。
-
核心特性
- 同步阻塞加载:require() 读取本地文件,文件未读取完成会阻塞代码执行,适合本地磁盘,不适合浏览器网络请求;
- 运行时动态加载:代码执行阶段才解析依赖,无法在编译期做静态分析;
- 导出机制:module.exports 为主导出对象,exports 是其浅层引用;
- 导入方式:require('文件/第三方包路径');
- 值拷贝导出:导出数据为副本,模块内部变量更新,外部导入值不会同步变化;
- 模块缓存:模块首次加载后存入缓存,重复引入仅执行一次模块代码。
- 代码示例
js
// math.js
const add = (a, b) => a + b;
module.exports = { add };
// index.js
const math = require('./math');
console.log(math.add(1, 2));
示例2:
js
// 1 通过exports 暴露模块接口:
// study.js
var hello = function () {
console.log('hello studygd.com.');
}
exports.hello = hello;
// main.js
const studygd = require('./study');
studygd.hello();
//2、通过module.exports 暴露模块接口:定义math模块
//math.js
module.exports = {
add: function(left, right) {
return left + right;
},
subtract: function(left, right) {
return left - right;
}
}
//使用刚才定义的math模块, 并再定义一个calculator模块
//calculator.js
const math = require('./math.js');
module.exports = {
add: math.add
}
- 优缺点
- 优点:Node 原生支持、API 简单、完善缓存机制、后端生态成熟;
- 缺点:同步阻塞不适合浏览器、不支持 Tree-Shaking、仅值拷贝绑定。
- 适用场景
Node.js 后端项目、打包工具输出 dist/cjs 产物。
三、AMD(Asynchronous Module Definition 异步模块定义)
- 诞生背景
为浏览器网络异步加载设计,代表工具:RequireJS,解决浏览器同步加载阻塞页面问题。 - 核心特性
- 异步并行加载:依赖并行发送网络请求,不会阻塞页面渲染;
- 依赖前置声明:所有依赖必须写在 define 第一个数组参数中;
- 执行机制:所有依赖全部下载完成后,才执行模块回调函数。
- 代码示例
js
// 定义模块
define(['jquery', './math'], function ($, math) {
function render() {
$('body').text(math.add(1, 3));
}
// 导出模块
return { render };
});
// 入口加载
require(['./main'], function (app) {
app.render();
});
示例2:
js
/// 1.通过define方法定义模块base.js
define(function (){
var control = {};
return control;
});
/// control.js
define(['jquery', 'jqmd5', 'cookie', 'base'], function (){
var control = {};
/**
* 登录状态检测
*/
control.cookie = function (){
setTimeout(WK.LC.syncLoginState, 100);
};
/**
* 模块调用及配置
*/
control.template = function (){
if($('.naver').length > 0) base.naver();
if(CATEGORY == 'login')
{
if(MODEL == 'index'){
// 登录页
require(['login'], function (Login){
Login.form();
});
};
if(MODEL == 'register' || MODEL == 'check'){
// 注册页
require(['register'], function (Register){
Register.form(MODEL);
});
};
};
if(CATEGORY == 'goods')
{
// 详情页
if(MODEL == 'index'){
require(['detail'], function (Detail){
// Detail.form();
});
};
};
};
return control;
});
// 2. 通过require方法加载模块(异步加载); 注意:参数里面有define声明的模块
require(['control'], function (Control){
Control.cookie();
Control.template();
});
- 优缺点
优点:浏览器异步无阻塞、支持按需分块加载;
缺点:语法繁琐冗余、依赖前置可读性差、无法兼容 Node、生态逐步淘汰。 - 适用场景
老旧 RequireJS 前端项目,目前基本不再使用。
四、CMD(Common Module Definition 通用模块定义)
- 诞生背景
国内阿里 SeaJS 推出,改良 AMD 语法,兼顾 CommonJS 书写习惯与浏览器异步加载。
在定义模块方面, CMD和AMD一样通过define函数来定义模块; 两者的主要区别在于对依赖的加载上, CMD中不需要在define的参数中直接声明需要用到的模块 - 核心特性
- 底层依旧异步加载,但依赖就近、延迟执行(与 AMD 核心区别);
- 无需顶部一次性声明全部依赖,代码用到时再 require 引入;
- 内置 require / exports / module,写法贴近 CommonJS。
- 代码示例
js
define(function (require, exports, module) {
// 就近按需引入依赖
const math = require('./math');
exports.log = function () {
console.log(math.add(2, 4));
};
});
示例2:
js
// 1.通过define方法定义模块calculator.js
define('calculator', function(require, exports) {
// 通过require方法加载其他模块
var math = require('math');
exports.add = function(left, right) { return math.add(left, right) };
exports.subtract = function(left, right) { return math.subtract(left, right) };
})
//可以看到calculator模块所的依赖的math模块没有在define函数的参数中进行声明, 而是通过require('math')来引入的
//2、使用calculator模块
seajs.use(['calculator'], function(calculator) {
console.log('1 + 1 = ' + calculator.add(1, 1));
console.log('2 - 2 = ' + calculator.subtract(2, 1));
})
- AMD vs CMD 核心对比
| 对比维度 | AMD(RequireJS) | CMD(SeaJS) |
|---|---|---|
| 依赖书写位置 | 文件顶部统一前置声明 | 代码就近,用到再引入 |
| 依赖执行时机 | 全部依赖下载完成统一执行 | 下载完成后,调用 require 时才执行 |
| 代表工具 | RequireJS | SeaJS |
- 优缺点
- 优点:书写贴近 CommonJS、按需加载,开发体验友好;
- 缺点:国内小众规范、生态贫瘠、无现代打包工具原生支持,已彻底淘汰。
- 适用场景
多年前 SeaJS 遗留老项目,新项目禁止使用。
五、UMD(Universal Module Definition 通用模块定义)
- 诞生背景
为第三方类库设计,实现一套代码兼容 CommonJS / AMD / 浏览器全局 script(CMD) 三种环境。
amd cmd 通常只能在浏览器中使用, commonjs只能在服务端(Node)**环境下使用, 这样子搞会导致我们基于其中某一种模块规范写的js模块无法在服务端和浏览器端进行复用.
umd解决了这个问题, 它兼容并包, 使得使用此规范写的 js模块既可以在浏览器环境下使用, 也可以在Node(服务端)环境中用
- 核心特性
通过自执行函数自动判断当前运行环境,自适应不同模块化规范导出。
- 标准 UMD 模板
js
(function (root, factory) {
// AMD 环境判断
if (typeof define === 'function' && define.amd) {
define(['jquery'], factory);
}
// CommonJS / Node 环境判断
else if (typeof module === 'object' && module.exports) {
module.exports = factory(require('jquery'));
}
// 普通浏览器 script 全局挂载
else {
root.myLib = factory(root.jQuery);
}
})(this, function ($) {
// 类库核心逻辑
return {
sayHi: () => console.log('hello umd lib')
};
});
示例2
js
(function (root, factory) {
if (typeof exports === 'object' && typeof module === 'object')
// commonjs
module.exports = factory()
else if (typeof define === 'function' && define.amd)
// amd、cmd
define([], factory)
else if (typeof exports === 'object')
// commonjs
exports['math'] = factory()
else
// 全局对象, 浏览器中是 window
root['math'] = factory()
})(this, function() {
return { add: function(left, right) { return left + right; } }
})
//其实只要你看过jq源码,你就会觉得 上面的这段代码很熟悉,是的,jq源码里面就是采用了umd规范去做兼容,所以jq可以说是umd规范的一种代表
//附:jq源码大体框架
( function( global, factory ) {
"use strict";
if ( typeof module === "object" && typeof module.exports === "object" ) {
// For CommonJS and CommonJS-like environments where a proper `window`
// is present, execute the factory and get jQuery.
// For environments that do not have a `window` with a `document`
// (such as Node.js), expose a factory as module.exports.
// This accentuates the need for the creation of a real `window`.
// e.g. var jQuery = require("jquery")(window);
// See ticket #14549 for more info.
module.exports = global.document ?
factory( global, true ) :
function( w ) {
if ( !w.document ) {
throw new Error( "jQuery requires a window with a document" );
}
return factory( w );
};
} else {
factory( global );
}
// Pass this if window is not defined yet
} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
//这里编写jquery主体代码...
// AMD
if ( typeof define === "function" && define.amd ) {
define( "jquery", [], function() {
return jQuery;
} );
}
var
// Map over jQuery in case of overwrite
_jQuery = window.jQuery,
// Map over the $ in case of overwrite
_$ = window.$;
jQuery.noConflict = function( deep ) {
if ( window.$ === jQuery ) {
window.$ = _$;
}
if ( deep && window.jQuery === jQuery ) {
window.jQuery = _jQuery;
}
return jQuery;
};
if ( !noGlobal ) {
window.jQuery = window.$ = jQuery;
}
return jQuery;
} );
- 优缺点
- 优点:一份代码适配浏览器、Node、AMD 多场景,第三方库通用标准;
- 缺点:封装模板冗余,仅适合工具类库,业务项目不推荐使用。
- 适用场景
第三方开源类库打包产物(lodash、axios 等 dist/umd 文件)。
六、ESM / ES Module(ES6 Module,现代官方标准)
- 诞生背景
ES2015 (ES6) 官方推出的原生模块化标准,浏览器、Node.js 双端原生支持,当前行业唯一主流规范。
使用import导入模块,通过export导出模块
- 核心特性
- 静态编译分析:import / export 必须写在文件顶层,编译期即可扫描依赖,原生支持 Tree-Shaking;
- 实时绑定导出:导出变量为动态引用,模块内部变量更新,外部导入同步生效(区别于 CommonJS 值拷贝);
- 默认开启严格模式 use strict;
- 区分静态导入(编译期)与动态导入函数 import()(运行时异步按需加载);
- 导入变量只读,外部无法修改导出值。
- 基础语法示例
js
// math.js 导出
export const add = (a, b) => a + b;
export const sub = (a, b) => a - b;
// 默认导出
export default { add };
// index.js 导入
import { add, sub } from './math.js';
import math from './math.js';
// 命名空间全部导入
import * as MathUtil from './math.js';
// 动态异步按需导入
import('./math.js').then(mod => console.log(mod.add(1, 2)));
- 浏览器使用方式
html
<!-- 必须声明 type="module" -->
<script type="module" src="main.js"></script>
-
Node.js 开启 ESM 两种方式
1.项目 package.json 添加配置:"type": "module";
2.文件后缀命名为 .mjs。
-
ESM 与 CommonJS 关键差异
| 特性 | CommonJS(CJS) | ESM |
|---|---|---|
| 加载阶段 | 运行时动态加载 | 编译期静态解析 |
| Tree-Shaking | 不支持 | 原生支持 |
| 导出绑定 | 值拷贝,无法同步更新 | 实时引用绑定 |
| 条件导入 | if/for 中可写 require | import 仅允许顶层,动态用 import () |
| 文件识别 | 默认 .js | .js 需配置 type:module /.mjs |
| 循环依赖处理 | 缓存快照,易丢失数据 | 实时绑定,循环依赖更稳定 |
- 优缺点
- 优点:官方统一标准、前后端通用、原生 Tree-Shaking、静态优化、支持动态导入、语法简洁;
- 缺点:老旧浏览器需转译、Node 低版本兼容差、导入路径必须补全文件后缀。
- 适用场景
Vue/React 现代前端项目、全新 Node 项目、浏览器原生开发、打包工具 dist/esm 产物。
示例
js
//math.js
export { add: (left, right) => left + right; }
// 在calculator.js导入
import { add } from './math.js';
console.log('1 + 1 = ' + add(1, 1));
// ES6 Module例子
//calculator.js
var count=0;
const ADD=function(a,b){
count+=1;
return a+b;
};
export {count,ADD}
//inde.html
<script type="module">
import {count,ADD} from './src/calculator.js'; // 导入calculator.js
console.log(count); // 0
ADD(2,3);
console.log(count); // 1
</script>
// ES6模块是动态引用,如果使用import从一个模块中加载变量(即import foo from 'foo'),那么,变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者保证在真正取值的时候能够取到值。
// 循环加载
// a.js
import {bar} from './b.js';
export function foo() {
console.log('foo');
bar();
console.log('执行完毕');
}
foo();
// b.js
import {foo} from './a.js';
export function bar() {
console.log('bar');
if (Math.random() > 0.5) {
foo();
}
}
//在浏览器中使用原生的 ESM
// 1 通过 script[type="module"] 可直接在浏览器中使用原生 ESM
<script type="module">
import arrayUniq from "https://cdn.jsdelivr.net/npm/array-uniq/index.js"
console.log(arrayUniq([1, 2, 3, 2, 3]))
// [1, 2, 3]
</script>
// 2 使用 script[type="importmap"] 统一配置导入路径
<script type="importmap">
{
"imports": {
"array-uniq": "https://cdn.jsdelivr.net/npm/array-uniq/index.js"
}
}
</script>
<script type="module">
import arrayUniq from "array-uniq"
console.log(arrayUniq([1, 2, 3, 2, 3]))
// [1, 2, 3]
</script>
// 3 使用 assert 指定导入文件的类型
<script type="module">
import data from "./data.json" assert { type: "json" };
console.log(data)
// {name: 'Tom', age: '25'}
</script>
// data.json
{
"name": "Tom",
"age": "25"
}
七、五大模块规范横向总对比表
| 规范 | 全称 | 设计环境 | 加载方式 | 核心API | 代表工具 | 当前状态 |
|---|---|---|---|---|---|---|
| CommonJS | 通用 JS 模块 | Node 服务端 | 同步阻塞 | require / module.exports | Node、Webpack CJS | 后端 / 旧库仍在用 |
| AMD | 异步模块定义 | 浏览器 | 异步、依赖前置 | define / require | RequireJS | 已淘汰 |
| CMD | 通用模块定义 | 浏览器 | 异步、依赖就近 | define + 内部 require | SeaJS | 完全淘汰 |
| UMD | 通用模块封装 | 全环境兼容 | 自适应判断 | 自执行环境判断 | 第三方类库打包 | 仅库产物使用 |
| ESM | ES6 原生模块 | 浏览器 + Node | 静态同步 + 动态异步 | import / export | Vite、Webpack、原生浏览器 | 现代唯一主流标准 |
八、工程化打包产物对应规范说明
- dist/cjs:CommonJS 格式,供 Node 环境直接引入;
- dist/esm:ES Module 格式,供现代打包工具做 Tree-Shaking 优化;
- dist/umd:UMD 通用格式,兼容所有老旧环境,可直接通过 script 标签引入。
九、模块化发展演进路线
无模块化(全局变量混乱)
- → CommonJS (Node) / AMD (浏览器) 并行
- → CMD(国内改良 AMD,短期过渡)
- → UMD(多环境兼容类库方案)
- → ESM(官方统一标准,未来唯一标准)
现在常用的模块规范一般就是es6模块和commonjs(只用于node)了, node中也已经提供了实验性的es模块支持.