JS模块化规范
什么是模块化
模块化就是将系统分离成独立功能的模块,这样我们需要什么功能,就加载什么功能,而不是把所有功能都加载进来。通常来说,一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。目前流行的js模块化规范有CommonJS
、AMD
、CMD
、UMD
以及ESM (ES)
的模块系统。
模块化的好处
避免命名空间的冲突(减少命名空间的污染)
更好的分离,实现按需加载
提高可代码的复用性
提高了代码的维护性
JS模块化的发展过程可以概括为以下几个阶段
CommonJS
:最初为服务器端 JavaScript 设计,通过 require 函数加载模块,通过 module.exports 或 exports 对象导出模块的内容。AMD
:AMD(Asynchronous Module Definition)规范是为解决浏览器端 JavaScript 模块化问题而提出,允许异步加载模块,提高浏览器端应用的性能。CMD
:CMD(Common Module Definition)规范是另一种浏览器端 JavaScript 模块化规范,与 AMD 类似,但更加简洁。UMD
:UMD(Universal Module Definition)规范是一种通用的模块定义规范,通过判断当前环境来选择合适的模块加载方式。ESM (ES)
:ESM(ECMAScript Module)是 ECMAScript 标准中的模块系统,它是 JavaScript 语言原生支持的模块化方案。
总的来说,JS模块化的发展过程是从服务器端到浏览器端,从同步到异步,从复杂到简单的过程。随着技术的不断发展,模块化的方式也在不断演进,以适应不同的开发需求。
CommonJS 规范
在 CommonJS 规范中,一个文件就是一个模块。模块内部定义的变量、函数或类都是私有的,只在模块内部可见。
使用
导出模块,需要使用 module.exports
或 exports
(不推荐直接用 exports
) 对象。
// moduleA.js
function add(a, b) {
return a + b;
}
module.exports = add;
导入模块,需要使用 require
函数。require
函数接受一个模块路径作为参数,并返回该模块导出的内容。
// moduleB.js
const add = require('./moduleA');
console.log(add(1, 2)); // Output: 3
exports 和 module.export 区别
exports:对于本身来讲是一个变量(对象),它不是 module
的引用,它是 {}
的引用,它指向 module.exports
的 {}
模块。只能使用 .
语法 向外暴露变量。
module.exports:module
是一个变量,指向一块内存,exports
是 module
中的一个属性,存储在内存中,然后 exports
属性指向 {}
模块。既可以使用 .
语法,也可以使用 =
直接赋值。
同步加载
CommonJS用同步的方式加载模块。在服务端,模块文件都存放在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,程序会阻塞到模块加载完成,更合理的方案是使用异步加载。
AMD
AMD,即Asynchronous Module Definition,是一种异步模块定义规范,它主要用于浏览器环境中的 JavaScript 模块化。AMD规范的出现是为了解决在浏览器中加载和管理模块的问题,特别是在处理大型应用程序时,需要一种高效的方式来异步加载模块,以提高性能和用户体验。
主要特点
异步加载:AMD允许模块异步加载,这意味着模块可以在需要时才被加载,而不是在页面加载时一次性加载所有模块。这样可以减少页面加载时间,提高应用程序的响应速度。
依赖管理:AMD规范提供了一种机制来管理模块之间的依赖关系。每个模块可以声明它所依赖的其他模块,AMD加载器会确保在执行该模块之前,所有依赖的模块都已经加载并准备就绪。
定义模块:在AMD中,模块通过
define
函数来定义。define
函数接受两个或三个参数:模块的ID、依赖的模块数组以及一个定义模块的工厂函数。工厂函数在所有依赖模块加载完成后执行,并返回模块的实例或接口。加载模块:AMD使用
require
函数来加载模块。require
函数接受两个参数:依赖的模块数组和一个回调函数。回调函数在所有依赖模块加载完成后执行,并接收这些模块的实例作为参数。
使用Require JS
目前,主要有两个Javascript库实现了AMD规范:require.js
和 curl.js
。
下面是使用Require JS的示例:
首先我们需要引入 require.js
文件和一个入口文件 main.js
。main.js
中配置 require.config()
并规定项目中用到的基础模块。
/** 网页中引入require.js及main.js **/
<script src="js/require.js" data-main="js/main"></script>
/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min", //实际路径为js/lib/jquery.min.js
"underscore": "underscore.min",
}
});
// 执行基本操作
require(["jquery","underscore"],function($,_){
// some code here
});
引用模块的时候,我们将模块名放在 []
中作为 reqiure()
的第一参数;如果我们定义的模块本身也依赖其他模块,那就需要将它们放在 []
中作为 define()
的第一参数。
// 定义math.js模块
define(function () {
var basicNum = 0;
var add = function (x, y) {
return x + y;
};
return {
add: add,
basicNum :basicNum
};
});
// 定义一个依赖underscore.js的模块
define(['underscore'],function(_){
var classify = function(list){
_.countBy(list,function(num){
return num > 30 ? 'old' : 'young';
})
};
return {
classify :classify
};
})
// 引用模块,将模块放在[]内
require(['jquery', 'math'],function($, math){
var sum = math.add(10,20);
$("#sum").html(sum);
});
CMD
CMD,即Common Module Definition,是另一种异步模块定义规范,它与AMD规范类似,但更加简洁。CMD规范的核心是通过 define
函数定义模块,通过 require
函数加载模块。
主要特点
异步加载:CMD允许模块异步加载,这意味着模块可以在需要时才被加载,而不是在页面加载时一次性加载所有模块。这样可以减少页面加载时间,提高应用程序的响应速度。
依赖管理:CMD规范提供了一种机制来管理模块之间的依赖关系。每个模块可以声明它所依赖的其他模块,CMD加载器会确保在执行该模块之前,所有依赖的模块都已经加载并准备就绪。
定义模块:在CMD中,模块通过
define
函数来定义。define
函数接受两个或三个参数:模块的ID、依赖的模块数组以及一个定义模块的工厂函数。工厂函数在所有依赖模块加载完成后执行,并返回模块的实例或接口。加载模块:CMD使用
require
函数来加载模块。require
函数接受两个参数:依赖的模块数组和一个回调函数。回调函数在所有依赖模块加载完成后执行,并接收这些模块的实例作为参数。
CMD 与 AMD的区别
AMD的实现者 require.js
在申明依赖的模块时,会在第一时间加载并执行模块内的代码:
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等于在最前面声明并初始化了要用到的所有模块
if (false) {
// 即便没用到某个模块 b,但 b 还是提前执行了。**这就CMD要优化的地方**
b.foo()
}
});
CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。
使用SeaJS
目前,主要有两个Javascript库实现了CMD规范:sea.js
和 curl.js
。
下面是使用SeaJS的示例:
首先我们需要引入 sea.js
文件和一个入口文件 main.js
。main.js
中配置 seajs.config()
并规定项目中用到的基础模块。
/** 网页中引入sea.js及main.js **/
<script src="js/sea.js" data-main="js/main"></script>
/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
seajs.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min", //实际路径为js/lib/jquery.min.js
"underscore": "underscore.min",
}
});
// 执行基本操作
seajs.use(["jquery","underscore"],function($,_){
// some code here
});
引用模块的时候,我们将模块名放在 []
中作为 seajs.use()
的第一参数;如果我们定义的模块本身也依赖其他模块,那就需要将它们放在 []
中作为 define()
的第一参数。
// 定义math.js模块
define(function (require, exports, module) {
var basicNum = 0;
var add = function (x, y) {
return x + y;
};
module.exports = {
add: add,
basicNum :basicNum
};
});
// 定义一个依赖underscore.js的模块
define(function(require, exports, module) {
var _ = require('underscore');
var classify = function(list){
return _.countBy(list,function(num){
return num > 30? 'old' : 'young';
})
};
module.exports = {
classify :classify
};
})
// 引用模块,将模块放在[]内
seajs.use(['jquery', 'math'],function($, math){
var sum = math.add(10,20);
$("#sum").html(sum);
});
UMD
UMD(Universal Module Definition)规范是一种通用的模块定义规范,它旨在提供一种可以在多种环境下(如 CommonJS、AMD、浏览器等)使用的模块定义方式。UMD 规范的核心是通过判断当前环境来选择合适的模块加载方式。
主要特点
通用性:UMD 规范可以在不同的 JavaScript 环境中使用,包括浏览器、Node.js 等。
环境判断:UMD 模块会通过判断当前环境来决定使用哪种模块加载方式。
兼容性:UMD 规范兼容 CommonJS 和 AMD 规范,因此可以在支持这些规范的环境中无缝使用。
简单UMD模块的实现
实现一个UMD模块,就要考虑现有的主流javascript模块规范了,如 CommonJS
, AMD
, CMD
等。那么如何才能同时满足这几种规范呢? 首先要想到,模块最终是要导出一个对象,函数,或者变量。
而不同的模块规范,关于模块导出这部分的定义是完全不一样的。因此,我们需要一种过渡机制。
首先,我们需要一个 factory
,也就是工厂函数,它只负责返回你需要导出的内容(对象,函数,变量等)。
我们从导出一个简单的对象开始。
function factory() {
return {
name: '我是一个umd模块'
}
}
全局对象挂载属性
假设不考虑 CommonJS
, AMD
, CMD
,仅仅将这个模块作为全局对象的一个属性应该怎么写呢?
(function(root, factory) {
console.log('没有模块环境,直接挂载在全局对象上')
root.umdModule = factory();
}(this, function() {
return {
name: '我是一个umd模块'
}
}))
我们把 factory
写成一个匿名函数,利用 IIFE
(立即执行函数)去执行工厂函数,返回的对象赋值给 root.umdModule
,这里的root就是指向全局对象 this
,其值可能是 window
或者 global
,视运行环境而定。
打开效果页面链接(要看源码的话,点开Git仓库),观察Network的文件加载顺序,可以看到,原则就是依赖先行。
再进一步,兼容AMD规范
要兼容 AMD
也简单,判断一下环境,是否满足 AMD
规范。如果满足,则使用 require.js
提供的 define
函数定义模块。
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// 如果环境中有define函数,并且define函数具备amd属性,则可以判断当前环境满足AMD规范
console.log('是AMD模块规范,如require.js')
define(factory)
} else {
console.log('没有模块环境,直接挂载在全局对象上')
root.umdModule = factory();
}
}(this, function() {
return {
name: '我是一个umd模块'
}
}))
打开效果页面链接,可以看到,原则是调用者先加载,所依赖的模块后加载。 ![](URL_ADDRESS
起飞,直接UMD
同理,接着判断当前环境是否满足 CommonJS
或 CMD
规范,分别使用相应的模块定义方法进行模块定义。
(function(root, factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
console.log('是commonjs模块规范,nodejs环境')
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
console.log('是AMD模块规范,如require.js')
define(factory)
} else if (typeof define === 'function' && define.cmd) {
console.log('是CMD模块规范,如sea.js')
define(function(require, exports, module) {
module.exports = factory()
})
} else {
console.log('没有模块环境,直接挂载在全局对象上')
root.umdModule = factory();
}
}(this, function() {
return {
name: '我是一个umd模块'
}
}))
最终,使用 require.js
, sea.js
, nodejs
或全局对象挂载属性等方式都能完美地使用 umd-module.js
这个模块,实现了大一统。
有依赖关系的UMD模块
全局对象挂载属性
这个简单,在html中你的模块前引入所依赖的模块即可。umd-module-depended
和 umd-module
都是UMD模块,后者依赖前者。
<!DOCTYPE html>
<html>
<head>
<title>Test UMD</title>
<!-- 依赖放前面 -->
<script src="assets/js/umd-dep/umd-module-depended.js"></script>
<script src="assets/js/umd-dep/umd-module.js"></script>
<script src="assets/js/umd-dep/umd-global.js"></script>
</head>
<body>
<h1>测试UMD模块</h1>
<h2></h2>
<p id="content"></p>
<p id="content2"></p>
</body>
</html>
AMD
在入口文件umd-main-requirejs.js
中,定义好模块路径,方便调用
require.config({
baseUrl: "./assets/js/umd-dep/",
paths: {
umd: "umd-module",
depModule: "umd-module-depended"
}
});
同理,CMD,Node.js环境也一样。
ESM (ES)
ESM(ECMAScript Modules)是 JavaScript 的官方模块化规范,它在 ECMAScript 6(ES6)中引入,并在现代浏览器和 Node.js 环境中得到广泛支持。ESM 规范提供了一种标准化的方式来组织和管理 JavaScript 代码,使得代码更加模块化、可维护和可重用。
主要特点
静态模块结构:ESM 使用静态模块结构,这意味着模块的依赖关系在编译时就已经确定,而不是在运行时动态解析。这有助于提高代码的性能和可预测性。
模块导入和导出:ESM 使用
import
和export
关键字来导入和导出模块。import 用于从其他模块导入功能,而export
用于将模块的功能暴露给其他模块。模块路径:ESM 使用相对路径或绝对路径来指定模块的位置。相对路径是相对于当前模块的位置,而绝对路径是相对于根目录的位置。
模块解析:ESM 模块的解析是由 JavaScript 引擎自动完成的,它会根据模块的路径和文件名来找到并加载相应的模块。
循环依赖:ESM 允许模块之间存在循环依赖,但需要注意的是,循环依赖可能会导致一些问题,如性能下降和代码复杂性增加。
简单ESM模块的实现
/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
return a + b;
};
export { basicNum, add };
/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
ele.textContent = add(99 + basicNum);
}
如上例所示,使用import命令的时候,用户需要知道所要加载的变量名或函数名。其实ES6还提供了 export default
命令,为模块指定默认输出,对应的 import
语句不需要使用大括号。这也更趋近于ADM的引用写法。
/** export default **/
//定义输出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {
ele.textContent = math.add(99 + math.basicNum);
}
ES6的模块不是对象,import
命令会被 JavaScript 引擎静态分析,在编译时就引入模块代码,而不是在代码运行时加载,所以无法实现条件加载。也正因为这个,使得静态分析成为可能。
ES6 模块的特征:
严格模式:ES6 的模块自动采用严格模式
import
read-only特性:import
的属性是只读的,不能赋值,类似于const的特性export/import
提升:import/export
必须位于模块顶级,不能位于作用域内;其次对于模块内的import/export
会提升到模块顶部,这是在编译阶段完成的