JavaScript 模块规范演进:从 CommonJS 到 ES6 的实战解析

作者:木棉时间:2026-10-23 06:36:18浏览:338

本文梳理 JavaScript 模块化发展的历史脉络,深入解析 CommonJS、AMD、CMD 和 UMD 等主流规范的原理与差异。重点对比 ES6 原生模块加载机制,结合 RequireJS 和 Sea.js 源码逻辑,说明各规范在浏览器与 Node.js 环境下的实现方式及兼容策略,帮助开发者理解模块加载的核心概念。

JavaScript 模块化演进概述

JavaScript 是一门极具灵活性的语言,甚至具备自我编译的能力。正是这种高度的自由度,催生了众多规范与框架,其中基础且关键的一环便是模块化(Module)。

不妨做一个小测试:CommonJS、AMD、CMD、UMD 以及 ES6,这些模块规范中,你熟悉几个?

需要说明的是,本文基于笔者个人的理解与梳理,可能与模块规范形成的确切历史存在一定差异。

模块概念的起源与挑战

一切的根源

JavaScript 本身在设计之初并未包含模块概念,但凭借极高的自由度,开发者可以通过立即执行函数表达式(IIFE)或构造函数来模拟模块行为,从而实现代码复用、作用域隔离及易于维护的效果。然而,这种散装的模块管理方式难以处理模块间的依赖关系。当一个 JS 文件中包含大量模块时,代码维护将变得异常困难,形成所谓的“灾难现场”。

主流模块规范诞生历程

Module 的诞生

为了解决这一问题,JavaScript 模块(JS Module)这一概念应运而生。由于 JS 原生缺乏模块支持,业界专家制定了 CommonJS 规范,实现了模块功能。但由于大多数浏览器不支持该规范,它主要适用于 Node.js 环境。随后,CommonJS 发生分裂,衍生出适用于浏览器端的 AMD 规范。由于 AMD 与 CommonJS 差异巨大,AMD 独立推出并推广了 RequireJS 框架。鉴于两者差异及适用环境不同,UMD(Universal Module Definition)随之出现,旨在兼容这两个平台,尽管笔者认为它更像是一个 polyfill。与此同时,CommonJS 的支持者尝试在浏览器端实现该规范,稍作改动后形成了 CMD 规范,并推出了 Sea.js 框架。当 AMD 与 CMD 争论不休时,ECMAScript 6(ES6)为 JS 引入了原生模块加载功能,宣告“JS 模块有了原生语法”。

真正规范与模拟环境的区别

真正的规范

在众多规范中,只有 CommonJS 和 ES6 的 import/export 属于真正的规范。其余规范均是利用 JS 现有特性模拟出的环境,以达成各自的目标。

之所以称 CommonJS 和 ES6 import/export 为真正规范,是因为只有原生支持其语法,才能完整实现规范定义。

对于 CommonJS 而言,运行环境必须提供 require 和 module.exports 支持才能运行。这也是浏览器与 CommonJS 难以直接兼容的主要原因。

至于 ES6,若失去对 import 和 export 关键字的支持,一切将归零。例如,Node.js 曾长期不支持 import 和 export,尽管它支持其他 ES6 语法。笔者认为,Node.js 为了维持 CommonJS 规范的稳定性,对 ES6 模块持保留态度,以免扰乱现有模块体系。

因此,CommonJS 和 ES6 模块被视为真正的规范。

关于 CommonJS 和 ES6 模块,笔者曾撰写过相关文章,此处不再赘述,感兴趣者可移步阅读《读懂 CommonJS 的模块加载》。

浏览器实现 CommonJS 的原理

有关浏览器实现 CommonJS 模块的原理

既然浏览器缺乏 CommonJS 所需的关键字导致模块无法直接运行,那么可以通过创建模块环境来解决。利用 define 方法,在函数内部模拟 CommonJS 环境,提供 require 和 module.exports 接口。无论是 Sea.js 还是 RequireJS,均通过 define 模拟环境的方式来实现模块功能。

AMD 规范与 RequireJS 解析

自立门户的 AMD

笔者此前在组装台式机时,曾在 A 卡(AMD)和 N 卡(NVIDIA)之间犹豫,最终因价格因素选择了 A 卡。这里的 AMD 指的是 AMD 公司,与 JS 中的 AMD 规范并无关联,仅是缩写巧合。

AMD 全称为 Asynchronous Module Definition(异步模块定义)。与 CommonJS 的按需加载(即 require 时才加载)不同,AMD 会在模块执行前加载所有潜在依赖,即所谓的“高配”模式,至于是否实际使用则不在 AMD 考虑范围内。RequireJS 是 AMD 的代表性实现:

来自 AMD 的暴击:


define("module1",function(require) {
 'use strict';
 console.log("cccc")
});
define("module2",function(require) {
 'use strict';
 console.log("aaaa")
 if(false){
  console.log(require("module1"))
 }
 console.log("bbbb")
});

require(["module2"])

此时打印顺序为 cccc, aaaa, bbbb。由此可见,AMD 会在模块执行前加载所有依赖。因此,AMD 还有一种写法是将所有依赖模块写在头部。


define("module1",function(require) {
 'use strict';
 console.log("cccc")
});
define("module2",[module1],function(module1) {
 'use strict';
 console.log("aaaa")
 if(false){
  console.log(require("module1"))
 }
 console.log("bbbb")
});

浏览一下 requirejs 的源码:

RequireJS 获取依赖有两种方式:一是通过配置,二是利用正则匹配所有 require 内容并加入依赖列表。当调用当前模块时,会先检查依赖模块是否已运行。


cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g,

已定义完成的模块会被缓存在一个对象中,以模块名为唯一键值。若再次调用该缓存模块,则无需重新执行。

执行之后缓存结果


defined[id] = exports;

二次执行时,先检查是否存在缓存,若存在则不重复执行。


function callGetModule(args) {
 //Skip modules already defined.
 if (!hasProp(defined, args[0])) {
  getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]);
 }
}

若是远程依赖,则创建一个 script 标签加载远程资源,并将其加入头部。


req.createNode = function (config, moduleName, url) {
 var node = config.xhtml ?
   document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :
   document.createElement('script');
 node.type = config.scriptType || 'text/javascript';
 node.charset = 'utf-8';
 node.async = true;
 return node;
};

UMD 兼容方案解析

那么 UMD 是个什么样的存在

初次接触 UMD 是在使用 Webpack 打包库时,面对 CommonJS、AMD 和 UMD 等多个选项,当时感到困惑。在不知情的情况下,又出现了一个模块规范,令人头疼。

来自 webpack 的凝视:


output: {
 path: path.join(__dirname),
 filename: 'index.js',
 libraryTarget: "umd",//此处是希望打包的插件类型
 library: "Swiper",
}

看一眼打包后的效果:


!function(root,callback){
"object"==typeof exports&&"object"==typeof module?//判断是不是nodejs环境
 module.exports=callback(require("react"),require("prop-types"))
 :
 "function"==typeof define&&define.amd?//判断是不是requirejs的AMD环境
  define("Swiper",["react","prop-types"],callback)
  :"object"==typeof exports?//相当于连接到module.exports.Swiper
   exports.Swiper=callback(require("react"),require("prop-types"))
   :
   root.Swiper=callback(root.React,root.PropTypes)//全局变量
}(window,callback)

这样一个 polyfill,瞬间兼容了 CommonJS、AMD 和全局模块。这就是 UMD,与其说是规范,不如说它是一个兼容层,支持多种模块规范。

CMD 规范与 AMD 的差异

where is CMD?

细心的读者可能发现,CMD 不知去向,Webpack 打包选项中也没有 CMD 模块选项。CMD 实际上是基于 CommonJS 规范改造,以支持浏览器端的一种规范。

主要说说他和 AMD 的主要区别吧:

require 关键字引入内容的执行顺序。AMD 是依赖提前加载的概念,而 CMD 是同步执行,遇到 require 后再执行当前模块。


define("c",function(require, exports, module) {
 console.log("bbb")
});
define("b",function(require, exports, module) {
 console.log("aaa")
 require("c")
 console.log("ccc")
});
seajs.use("b")

这样打印的就是 aaa,bbb,ccc。按照代码出现的顺序执行。

当然这是同步代码的区别,至于异步代码,CMD 和 AMD 均通过 script 标签 append 到 head 加载,存入模块对象中,然后根据 id 调用。不过 CMD 有一点不同,加了一个小小的优化:


if (!data.debug) {
 head.removeChild(node)
}

当代码加载完毕之后,并且缓存在模块之中之后,便在 head 之中删除了这个 script。

总结与展望

后记

借用鲁迅的一句话“世上本没有路,走的人多了也就成了路”。JS MODULE 的规范也是如此,用的人多了也就是默认的解决方案了。

JS MODULE 大战就写到这边吧,大家都不晓得这些模块的规范能够存活多久,但是概念都很好。所以好好学习概念,以后就算有新的规范出来了,和老规范一对比,找出不同点,加以分析,便能够轻松上手!

参考地址

不能忘记帮助笔者认知模块的文章们,谢谢大佬们:

RequireJS 官网关于 AMD 产生历史的解释。 《前端模块化开发那点历史》:Sea.js 作者对模块的看法,梳理了笔者对于 AMD 的困惑。 Overflow 上对于 AMD 和 RequireJS 的一个解释

以上所述是小编给大家介绍的 js module 详解整合,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对本站网站的支持!

作者最新文章