JS 五大模块标准完整详解:CommonJS / AMD / CMD / UMD / ESM

JS 五大模块标准完整详解:CommonJS / AMD / CMD / UMD / ESM

一、模块化出现背景

早期 JavaScript 不存在原生模块化方案,开发中存在三大痛点:

  • 全局变量污染,变量、函数命名冲突;
  • 依赖加载顺序无法自动管理,手动引入脚本极易出错;
  • 代码复用、拆分、维护成本极高。
  • 社区先后诞生多套模块化规范,最终 ES6 推出官方原生 ESM 统一标准。

五大规范可分为三类:

  • 服务端早期规范:CommonJS、UMD
  • 浏览器异步模块化:AMD、CMD
  • 现代官方原生标准:ESM(ES6 Module)

二、CommonJS(简称 CJS)

通过exports和module.exports来暴露模块中的内容。通过require来加载模块。

  1. 诞生背景

    2009 年随 Node.js 诞生,是 Node.js 内置默认模块规范,面向服务端本地文件设计。

  2. 核心特性

  • 同步阻塞加载:require() 读取本地文件,文件未读取完成会阻塞代码执行,适合本地磁盘,不适合浏览器网络请求;
  • 运行时动态加载:代码执行阶段才解析依赖,无法在编译期做静态分析;
  • 导出机制:module.exports 为主导出对象,exports 是其浅层引用;
  • 导入方式:require('文件/第三方包路径');
  • 值拷贝导出:导出数据为副本,模块内部变量更新,外部导入值不会同步变化;
  • 模块缓存:模块首次加载后存入缓存,重复引入仅执行一次模块代码。
  1. 代码示例
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
}
  1. 优缺点
  • 优点:Node 原生支持、API 简单、完善缓存机制、后端生态成熟;
  • 缺点:同步阻塞不适合浏览器、不支持 Tree-Shaking、仅值拷贝绑定。
  1. 适用场景

Node.js 后端项目、打包工具输出 dist/cjs 产物。

三、AMD(Asynchronous Module Definition 异步模块定义)

  1. 诞生背景
    为浏览器网络异步加载设计,代表工具:RequireJS,解决浏览器同步加载阻塞页面问题。
  2. 核心特性
  • 异步并行加载:依赖并行发送网络请求,不会阻塞页面渲染;
  • 依赖前置声明:所有依赖必须写在 define 第一个数组参数中;
  • 执行机制:所有依赖全部下载完成后,才执行模块回调函数。
  1. 代码示例
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();
});
  1. 优缺点
    优点:浏览器异步无阻塞、支持按需分块加载;
    缺点:语法繁琐冗余、依赖前置可读性差、无法兼容 Node、生态逐步淘汰。
  2. 适用场景
    老旧 RequireJS 前端项目,目前基本不再使用。

四、CMD(Common Module Definition 通用模块定义)

  1. 诞生背景
    国内阿里 SeaJS 推出,改良 AMD 语法,兼顾 CommonJS 书写习惯与浏览器异步加载。
    在定义模块方面, CMD和AMD一样通过define函数来定义模块; 两者的主要区别在于对依赖的加载上, CMD中不需要在define的参数中直接声明需要用到的模块
  2. 核心特性
  • 底层依旧异步加载,但依赖就近、延迟执行(与 AMD 核心区别);
  • 无需顶部一次性声明全部依赖,代码用到时再 require 引入;
  • 内置 require / exports / module,写法贴近 CommonJS。
  1. 代码示例
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));
})
  1. AMD vs CMD 核心对比
对比维度 AMD(RequireJS) CMD(SeaJS)
依赖书写位置 文件顶部统一前置声明 代码就近,用到再引入
依赖执行时机 全部依赖下载完成统一执行 下载完成后,调用 require 时才执行
代表工具 RequireJS SeaJS
  1. 优缺点
  • 优点:书写贴近 CommonJS、按需加载,开发体验友好;
  • 缺点:国内小众规范、生态贫瘠、无现代打包工具原生支持,已彻底淘汰。
  1. 适用场景
    多年前 SeaJS 遗留老项目,新项目禁止使用。

五、UMD(Universal Module Definition 通用模块定义)

  1. 诞生背景

为第三方类库设计,实现一套代码兼容 CommonJS / AMD / 浏览器全局 script(CMD) 三种环境

amd cmd 通常只能在浏览器中使用, commonjs只能在服务端(Node)**环境下使用, 这样子搞会导致我们基于其中某一种模块规范写的js模块无法在服务端和浏览器端进行复用.

umd解决了这个问题, 它兼容并包, 使得使用此规范写的 js模块既可以在浏览器环境下使用, 也可以在Node(服务端)环境中用

  1. 核心特性

通过自执行函数自动判断当前运行环境,自适应不同模块化规范导出。

  1. 标准 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;
} );
  1. 优缺点
  • 优点:一份代码适配浏览器、Node、AMD 多场景,第三方库通用标准;
  • 缺点:封装模板冗余,仅适合工具类库,业务项目不推荐使用。
  1. 适用场景
    第三方开源类库打包产物(lodash、axios 等 dist/umd 文件)。

六、ESM / ES Module(ES6 Module,现代官方标准)

  1. 诞生背景
    ES2015 (ES6) 官方推出的原生模块化标准,浏览器、Node.js 双端原生支持,当前行业唯一主流规范。

使用import导入模块,通过export导出模块

  1. 核心特性
  • 静态编译分析:import / export 必须写在文件顶层,编译期即可扫描依赖,原生支持 Tree-Shaking;
  • 实时绑定导出:导出变量为动态引用,模块内部变量更新,外部导入同步生效(区别于 CommonJS 值拷贝);
  • 默认开启严格模式 use strict;
  • 区分静态导入(编译期)与动态导入函数 import()(运行时异步按需加载);
  • 导入变量只读,外部无法修改导出值。
  1. 基础语法示例
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)));
  1. 浏览器使用方式
html 复制代码
<!-- 必须声明 type="module" -->
<script type="module" src="main.js"></script>
  1. Node.js 开启 ESM 两种方式

    1.项目 package.json 添加配置:"type": "module";

    2.文件后缀命名为 .mjs。

  2. ESM 与 CommonJS 关键差异

特性 CommonJS(CJS) ESM
加载阶段 运行时动态加载 编译期静态解析
Tree-Shaking 不支持 原生支持
导出绑定 值拷贝,无法同步更新 实时引用绑定
条件导入 if/for 中可写 require import 仅允许顶层,动态用 import ()
文件识别 默认 .js .js 需配置 type:module /.mjs
循环依赖处理 缓存快照,易丢失数据 实时绑定,循环依赖更稳定
  1. 优缺点
  • 优点:官方统一标准、前后端通用、原生 Tree-Shaking、静态优化、支持动态导入、语法简洁;
  • 缺点:老旧浏览器需转译、Node 低版本兼容差、导入路径必须补全文件后缀。
  1. 适用场景
    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模块支持.