前言
使用JavaScript模块很久了,也知道js模块化的标准CommonJS
AMD
CMD
及现在的ES Module
写法,一直理不清他们之前的关系及差异。抽时间学习研究记录下。
发展历史
在JavaScript发展的初期,其实JS做一个脚本语言只用于辅助实现页面特效及交互。所有JS的脚本都执行在一个环境变量中(即window全局对象)且脚本的执行顺序是从上到下运行的。但是随着程序的复杂化,程序的全局变量越来越多。造成变量的混乱及相互污染,越来越难以维护等问题。
随着JavaScript和Node.js发展,JavaScript才慢慢有了模块化的方案。不同的方案也在不同端有自己的发展。
在讲解模块话的发展之前,需要说一下JavaScript中一个特别有用的函数表达式IIFE
(立即调用函数表达式)。引用MDN上的解释:
IFE(立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。
(function () {
statements
})();
这是一个被称为 自执行匿名函数 的设计模式,主要包含两部分。第一部分是包围在 圆括号运算符 () 里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。
第二部分再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。
看到这个结构是不是让你想起了什么,没错jQuery,jQuery的标准写法代码如下:
(function($) {
// other code here
$(document).ready(function() {
// other code here
});
})(jQuery);
正是因为IFFE函数的独立的词法作用域这种特性,在JavaScript没有统一模块化之前,相当长的一段时间在浏览器端支持模块化都是使用此类技术,在现在对于使用Babel、webpack、vite之类的工具让浏览器端执行CommonJS代码也是以此为基础来实现。
浏览器端
- AMD (Asynchronous Module Definition) – RequireJS
- CMD (Common Module Definition)- SeaJS
- UMD (Universal Module Definition)
- ES Modules
Node.js
- CommonJS (2009)
- ES Modules
模块化的介绍就按时间线来说吧,在程序发展的复杂程序上,后端往往大于前端。最初的成熟模块化方案也是出现于后端:CommonJS 。
CommonJS
这个标准最早是由Mozilla工程师Kevin Dangoor于2009年1月发起的,其目标是为JavaScript在网页浏览器之外建立模块约定。在CommonJS的规范中,每个js文件都有一个独立的上下文(module context,在Node.js环境下由Node.js提供),文件中所有变量(包括类和函数)都是创建在该对象的下面的私有属性。 要想暴露这些属性需要使用关键字 exports
, 即通过module.exports.x = xxx,进行导出。
let face = '?';
module.exports = {
feeling: '?',
face: face
}
const util = require('./util');
console.log(util) // { feeling: '?', face: '?' }
console.log(util.face) // ?
console.log(util.feeling)// ?
以上就是一个最简单的CommonJS例子,其中的module和require都是由Node.js本身提供。更详细的介绍可以查看这里。但是需要注意的一点就是:CommonJS最早是为服务端设计,且本身依赖的加载是同步的。如果到浏览器的场景下使用,一个模块的实现可能依赖于N个模块。也就是说浏览器瞬时可以会加载N+1模块文件,等待加载完成再来解析页面。对于浏览器来说需要一个异步加载的模块化规范,这就是 AMD。
AMD
AMD即Asynchronous Module Definition,“异步模块化定义”。看下基本的定义:
define(id?, dependencies?, factory);
说明:
- id 为该模块的标识字段
- dependencies 一般为数组,表示该模块的依赖模块
- factory 是一个函数,为对应依赖模块加载后返回的对象
对于AMD这个模块化标准,代表性框架就是:RequireJS。下以Require依赖的代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AMD-demo</title>
<script
defer
async="true"
data-main="js/main"
src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.min.js"
></script>
</head>
<body></body>
</html>
requirejs(['math'], function(math) {
console.log(math.add(1, 2))
})
define('math', function() {
var add = function(a, b) {
return a + b;
}
return {
add: add
}
});
最终运行的结果为:
且网络对应的加载文件顺序如下:
Dom结构
总结:从上面的例子可以有如下结果,AMD模块在加载时使用的是异步加载方式,每个模块会通过appendChild将模块,插入到DOM结构中,且在对应的脚本加载完成之后,才会执行对应逻辑代码。define
在整个AMD模块规则中,既是引用模块的一种方式,也是定义模块的关键字。 支持异步加载是AMD与CommonJS最不同的地方。
CMD
说完了AMD,就不得不说下玉伯的CMD和SeaJS。初衷是让 CommonJS Modules/1.1 模块也能便捷快速迁移运行在浏览器端(NodeJS 的生态繁荣),因此更贴近 CommonJS 规范。 上一个简单的模块代码
define(function(require, exports) {
exports.add = function(a, b) {
return a + b;
}
});
是不是和AMD的写法都傻傻分不清楚了,都是如下的语法:
define(id?, deps?, factory)
其中的 id
,deps
是可以省略的,由工具自动生成。需要注意的是:id
和deps
并不是CMD
的规范。是而属于 Modules/Transport 规范(也可以参考知乎文章)。从代码中我们可以看到CMD提倡使用exports
或者module.exports
进行导出对象(当然也可以使用return)。更多对于CMD的说明可以参考这里。
CMD与AMD的区别:
- AMD 是提前执行, CMD 是延迟执行(as lazy as possible)( 可以从network面板查看文件加载顺序)
- CMD 提倡 依赖就近, AMD提倡依赖前置
-
- AMD
requirejs(['math', 'increment'], function(math) {
console.log(math.add(1, 2))
})
-
- CMD
define(function(require) {
// 先require就会先加载该文件
var add = require('./add').add;
console.log(`2 + 3 = `, add(2,3))
var inc = require('./increment').increment;
console.log(`5 - 3 = `, inc(5,3))
});
- AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。
更多可参考:github.com/seajs/seajs…
UMD
UMD(Universal Module Definition)即“通用模块话定义”,该规范的出现,主要为了让一个模块的代码能兼容AMD(CMD)和CommonJS这两种规范。上代码
(function(root, factory){
if(typeof module === 'object' && module.exports) { // CommonJS
console.log('执行了CommonJS规范')
module.exports = factory()
}
// 先判断AMD规范
// @see https://github.com/favrio/notes/issues/4
else if(typeof define === 'function' && define.amd) {
console.log('执行了AMD规范')
define(factory)
} else if (typeof define === "function" && define.cmd) {
// @see https://github.com/Ziphwy/Blog/issues/8
console.log('执行了CMD规范')
define(function(require, exports, module) {
module.exports = factory();
});
} else { // 如果都不是以上模块环境,就封装成 IIFE 模块
console.log('window Global')
root.returnExports = factory()
}
}(this, function() {
function Time() {
this._date = new Date();
}
return {
Time: Time,
name: 'umd-depended'
}
}))
(function(root, factory){
typeof define === 'function' && (define.amd) ? define(['umdModuleDepended'], factory) :
typeof define === 'function' && (define.cmd) ? define(function(require, exports){
exports = factory(require('./umdModuleDepended'))
}):
typeof module === 'object' && module.exports ? module.exports = factory(require('./umdModuleDepended')) : root.returnExports = factory(root.returnExports)
}(this, function(umdModule) {
console.log(this)
console.log('我调用了依赖模块', umdModule)
return {
name: 'umd-module'
}
}))
从代码可以看出,是通过判断不同的执行环境,进而执行不同规范的代码。最后默认为浏览器环境。从而达到写一次,上面所有规范全包。缺点就是代码看起来有点臃肿。
ESM(ES Module)
百家绽放的模块化规范,终于在2015年的版本 ECMAScript 2015(ES6)进行了统一。官方发布自己的模块化规范。
在ESM标准里使用import
引入其它模块的内容,使用export
来导出模块
export function add (a, b) {
return a + b;
}
export function increment (a, b) {
return a - b;
}
import { add } from "./add.js";
import { increment } from "./increment.js";
console.log('3 + 2 = ', add(3,2))
console.log('3 - 2 = ', increment(3, 2))
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ESM-demo</title>
<!-- 浏览器里加载模块必须使用 type="module" -->
<script type="module" src="js/index.js"></script>
</head>
区别
最后总结下CommonJS和ESM之间的区别:
- ESM的导出是值引用导出,如果通过方法修改了导出的值,其它地方再次导入会使用新值,而CommonJS是值导出(即复制一份) 。
// add.js
export let addInit = 1;
export function computeInit() {
addInit++
}
// index.js
console.log('index.js addStr = ', addInit) // index.js addStr = 1
computeInit()
console.log('index.js addStr = ', addInit) // index.js addStr = 2
// utils.js
let face = '?';
function changeFace () {
face = '?';
}
module.exports = {
face: face,
changeFace: changeFace
}
// index.js
const util = require('./util');
console.log(util.face) // ?
util.changeFace()
console.log(util.face) // ?
- CommonJS是运行时加载,ESM是编译时加载
- CommonJS的require是同步加载,而ESM的import时异步加载,有一个独立的模块解析阶段