前端模块化——CommonJS、AMD、CMD、ES6规范

什么是模块化?
什么是模块?
       对于一个复杂的程序,将其按照一定的规范封装成几个文件块,每一块向外暴露一些接口,块的内部数据是私有的,块与块之间通过接口通信。这个过程称为模块化,而文件块称为模块。

模块化的前世今生
模仿命名空间进行简单对象封装
       这种方法减少了全局变量的污染,减少了命名冲突。但与此同时,这种方法的问题是,由于是使用简单对象封装,对象外部能直接修改对象的属性,数据安全性不好。
let namespace={
data:‘123456’,
foo(){
console.log(ns foo ${data});
},
bar(){
console.log(ns bar ${data});
}
}
namespace.data=‘789456’;
namespace.foo(); //ns foo 789456
namespace.bar(); //ns bar 789456
1
2
3
4
5
6
7
8
9
10
11
12
通过闭包进行封装
       这种方法在减少全局变量冲突的同时还保证了私有数据的安全性,闭包外部只能通过暴露的接口操作私有数据。
       那么如果当前模块依赖另一个模块怎么办?答案是将被依赖的模块作为参数传入。这种方法是实现现代模块化的基础。
// module.js文件
(function(window, $) {
let data = ‘123456’;
function foo() {
console.log(mo foo ${data});
$(‘body’).css(‘background’, ‘black’);
}
function bar() {
console.log(mo bar ${data});
other(); //内部调用
}
function other() {
console.log(mo other ${data});
}
//暴露行为
window.mo = { foo, bar };
})(window, jQuery);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// index.html文件

1
2
3
4
5
6
7
模块化的好处
减少全局变量污染
提高了可复用性
代码更易维护
模块分离可以实现按需加载
一定程度上减少了http请求的数量
       模块化有着如此独特的好处,那么我们没有理由不对我们的代码进行模块化管理,下面我们来看一下四种主流的模块化规范,分别是CommonJS、AMD、CMD和ES6规范。

四种主流的模块化规范
CommonJS
1.概述
       采用CommonJS模块规范的应用,每个文件就是一个模块,具有自己的私有作用域,不会污染全局作用域。模块的加载是同步的而且可以加载多次,但在第一次加载后就会被缓存,后面再次被加载时会直接从缓存中读取。CommonJS主要用于服务器Node.js编程。
2.module对象
       每个模块内部都有一个module对象代表当前模块。module模块具有以下几个属性:

module.id:模块的标识符,通常是绝对路径的模块文件名。
module.filename:模块的文件名。
module.loaded:一个布尔值,表示模块是否已经完成加载。
module.parent:一个数组,表示依赖该模块的模块。
module.children:一个数组,表示该模块依赖的模块。
module.exports:一个对象,表示模块向外暴露的内容。
3.module.exports属性
       我们已经知道,module.exports属性是模块暴露的接口,加载某个模块时就是加载该模块的module.exports属性。所以module.exports属性是CommonJS模块化规范的核心。
4.exports变量
       每个module都拥有一个exports变量,它指向module.exports属性,是module.exports的引用,设置这个变量是为了能够给module.exports添加属性,使用该变量添加的属性对调用该模块的模块可见。

const exports=module.exports; // CommonJS规范在每个module前隐式做了这个赋值
1
5.基本语法

暴露模块
module.exports=value或者module.xxx=value
引入模块
require(xxx),如果引入的模块是第三方模块,那么xxx为模块名。如果引入的模块是自定义模块,那么xxx为文件路径。
6.实战

// module_exports_example.js
let x = 5;
let func = function () {
console.log(func);
};
module.exports.x = x;
module.exports.func = func;
// 等价于module.exports={x,func};
1
2
3
4
5
6
7
8
// require_example.js
let example=require(./module_exports_example);// 后缀名默认为.js
console.log(example.x);// 5
example.func();// func
1
2
3
4
       上面的例子给出了最简单CommonJS规范的例子,通过module.exports向外暴露模块,通过require在一个模块中引入以来的模块。下面我们来看一下exports变量的具体应用场景。

// module_exports_example.js
let x = 5;
let y = 5;
let func = function () {
console.log(func);
};
module.exports={x,func};
// 此时仍然想要向输出的对向添加属性可以这样做
exports.y = y;// 其实就是简单的为exports对象添加新的属性
1
2
3
4
5
6
7
8
9
// require_example.js
let example=require(./module_exports_example);// 后缀名默认为.js
console.log(example.x);// 5
console.log(example.y);// 5
example.func();// func
1
2
3
4
5
       上述例子使用了exports变量给module.exports对象添加属性,其效果等价于:

// module_exports_example.js
let x = 5;
let y = 5;
let func = function () {
console.log(func);
};
module.exports={x,y,func};// 直接使用module.exports
// 也等价于module.exports.x=x; module.exports.y=y; module.exports.func=func;
1
2
3
4
5
6
7
8
       突然一个鬼魅的操作涌上心头,如果两个模块循环引用会出现什么情况呢?如果模块的加载是简单的同步加载,那循环引用就会引起死循环,我们来试一下。

// a.js
exports.x = ‘a1’;
console.log(‘a.js ‘, require(’./b.js’).x);
exports.x = ‘a2’;

// b.js
exports.x = ‘b1’;
console.log(‘b.js ‘, require(’./a.js’).x);
exports.x = ‘b2’;

// main.js
console.log(‘main.js ‘, require(’./a.js’).x);
console.log(‘main.js ‘, require(’./b.js’).x);
1
2
3
4
5
6
7
8
9
10
11
12
13
       执行main.js后输出结果为:

b.js a1
a.js b2
main.js a2
main.js b2
1
2
3
4
       上述代码中a加载了b,b加载了a,但我们发现,并没有发生我们担心的循环引用的问题,这是因为CommonJS会在发生循环的位置剪断循环。具体的执行过程是这样的:
       执行第一句输出,但由于require是同步加载,会先转去加载a.js,在加载完成前不会输出。在加载a.js的过程中转去加载b.js,这时发现b,js加载了a.js,发生了循环引用,CommonJS在发生循环的点,也就是a.js的第二句话处切断循环,也就是说b.js加载a.js的时候不会执行exports.x=‘a2’。此时b.js加载a.js完毕,加载的a.x值是a1,所以先输出b.js a1,b.js文件继续向下执行,将其暴露的属性b.x修改为b2,b.js文件执行完毕,显然此时a.js加载的b.x为b2,所以输出第二行a.js b2,最后输出得到main.js a2和main.js b2。
       我们将上面的main.js文件稍微改一下:

// main.js
console.log(‘main.js ‘, require(’./a.js’).x);
console.log(‘main.js ‘, require(’./b.js’).x);
console.log(‘main.js ‘, require(’./a.js’).x);
console.log(‘main.js ‘, require(’./b.js’).x);
1
2
3
4
5
       执行一下,结果如下:

b.js a1
a.js b2
main.js a2
main.js b2
main.js a2
main.js b2
1
2
3
4
5
6
       似乎跟我们的预期不太一样,为什么第二次加载的时候不会执行a.js和b.js文件里的输出了呢?我们刚刚说过,模块可以多次加载,在第一次加载以后再加载模块时,会直接从缓存中取值而不会再次加载文件,所以第二次加载的时候会直接从缓存中取出exports属性,所以a.js和b.js文件的console.log语句不会执行了。
       另外还需要注意的一点是,模块内部的变化不会影响加载后的值,也就是说模块内部的属性和输出的属性不是响应式变化的,我们看一个例子:

// example.js
let num=0;
function inc(){
num++;
}
module.exports={num,inc};
1
2
3
4
5
6
let num=require(./example).num;
let inc=require(./example).inc;
console.log(num);// 0
inc();
console.log(num);// 0
1
2
3
4
5
       例子很简单就不做过多解释了。

AMD
1.概述
       AMD全称为Asynchronous Module Definition,是异步加载模块的,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不需要异步加载,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用异步模式,因此浏览器端一般采用AMD规范。
2.基本语法

定义模块
       AMD规范使用define来定义模块,define函数的定义是define(id?,dependencies?,factory)。id为字符串类型唯一用来标识模块(可以省略),dependencies是一个数组字面量,用来表示当前定义的模块所依赖的模块(默认后缀名是.js),当没有dependencies时,表示该模块是一个独立模块,不依赖任何模块。factory是一个需要实例化的函数,函数的参数与依赖的模块一一对应,函数需要有返回值,函数的返回值表示当前模块暴露的内容。
调用模块
       AMD规范使用require来调用模块,require函数的定义是require(dependencies,factory)。dependencies是一个数组字面量,表示调用的模块,factory需要传入一个回调函数,用来说明模块异步加载完成后执行的操作。
3.配置require对象
       require函数本身也是一个对象,它带有一个config函数用来配置require函数的运行参数,config函数接受一个对象作为参数。config的参数对象有以下几个属性:

baseUrl
       baseUrl参数指定本地模块位置的基准目录,即本地模块的路径是相对于哪个目录的。
paths
       paths参数指定各个模块的位置。这个位置可以是服务器上的相对位置,也可以是外部源。可以为每个模块定义多个位置,如果第一个位置加载失败,则加载第二个位置,后面我们将看到具体例子。
shim
       有些库不是AMD兼容的,这时就需要指定shim属性的值。shim是用来帮助require.js加载非AMD规范的库。
4.实战
       跟前面一样,还是先给出一个最基本的符合AMD规范模块化的例子。

// module1.js
// 定义一个没有依赖的模块
define(function() {
let msg = ‘123456’;
function get() {
return msg;
}
return { get }; // 暴露模块
})
1
2
3
4
5
6
7
8
9
// module2.js
// 定义一个依赖module1的模块
define([‘module1’],function(module1) {
let msg = ‘7890’;
function con() {
return module1.get()+msg;// 将两个字符串连接起来
}
return { con }; // 暴露模块
})
1
2
3
4
5
6
7
8
9
// main.js
function(){
require.config({
baseUrl:’./modules/’,
paths:{
module1:‘module1’,
module2:‘module2’
},
});
require([module2],function(module2){
console.log(module2.con());
});
}();// 1234567890
1
2
3
4
5
6
7
8
9
10
11
12
13
// index.html

AMD 1 2 3 4 5 6 7 8 9 10 11        此外,如何在项目中引入第三方库呢?只需要在上述代码中作少许修改:

// module2.js
// 定义一个依赖module1的模块
define([‘module1’,‘jquery’],function(module1,$) {
let msg = ‘7890’;
function con() {
return module1.get()+msg;// 将两个字符串连接起来
}
$(‘body’);// 使用第三方库
return { con }; // 暴露模块
})
1
2
3
4
5
6
7
8
9
10
// main.js
function(){
require.config({
baseUrl:’./modules/’,
paths:{
module1:‘module1’,
module2:‘module2’,
jquery:‘xxxx’
},
});
require([module2],function(module2){
console.log(module2.con());
});
}();// 1234567890
1
2
3
4
5
6
7
8
9
10
11
12
13
14
       在module.js文件中引入第三方库,main.js文件中也要有相应的路径配置。此外,我们还可以给一个库指定多个地址以作备用地址,在前面的地址失效的时候使用备用地址,例:

require.config({
paths: {
jquery: [
// 路径定义为数组,数组的每一项为一个地址定义
‘//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js’,
‘lib/jquery’
]
}
});
1
2
3
4
5
6
7
8
9
CMD
1.概述
       CMD全称是Common Module Definition,它整合了CommonJS和AMD规范的特点,专门用于浏览器端,异步加载模块。该规范明确了模块的书写格式和基本交互规则
2.基本语法
       CMD规范通过define关键字来定义模块,基本语法为define(factory),factory的取值可以是函数或者任何合法的值(对象、数组、字符串等)。当factory是函数时,表示是该模块的构造函数,这个函数具有三个参数————“require、exports、module”。require参数是一个方法,它接受模块唯一标识作为参数,用来引入依赖。exports参数用来暴露模块,module参数指向当前模块。
3.实战

CMD Demo 1 2 3 4 5 6 7 8 9 10 11 12 // module1.js // factory为对象 define({foo:foo}); 1 2 3 // module2.js // factory为字符串 define(foo:"123456"); 1 2 3 // module3.js // factory为数字 define(foo:666); 1 2 3        上面是factory为普通值时候的例子,下面我们来看一个当factory为函数的时,怎样使用:

// main.js
define(function(require) {
//通过riquire引用模块
var module1=require(’./module1’);
var module2=require(’./module2’);
var module3=require(’./module3’);
console.log(module1.foo);// foo
console.log(module2.foo);// 123456
console.log(module3.foo);// 666
});
1
2
3
4
5
6
7
8
9
10
// module4.js
// 定义一个没有依赖的模块
define(function(require,exports,module){
function CmdDefine(){
console.log(“CMD Demo”);
}
CmdDefine.prototype.say=function(){
console.log(“CMD Demo!”);
}
module.exports=CmdDefine;// 对外发布接口
});
1
2
3
4
5
6
7
8
9
10
11
// main.js
define(function(require) {
var CmdDefine=require(’./cmdDefine’);
var tmp = new CmdDefine();// 创建CmdDefine实例
tmp.say();// 调用say方法
});
1
2
3
4
5
6
       例子同样很容易理解,不做过多解释。
4.判断当前页面是否有CMD模块加载器

if(tepyof define === “function” && define.cmd){
// 有Sea.js等CMD模块加载器存在
}
1
2
3
ES6
1.概述
       ES6模块规范的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系。而CommonJS和CMD,都只能在运行时确定依赖。
2.基本语法

暴露接口
       export命令用于规定模块的对外接口,基本语法为export xxx,暴露的接口可以是对象、函数、基本类型变量。另外可以使用export default xxx为模块指定默认输出,因为很多时候用户不知道要加载的模块的属性名。
调用模块
       import命令用于输入其他模块提供的功能,基本语法为import xxx from xxx,其中第一个xxx为引入的模块的属性名,第二个xxx为模块的位置。如果不理解没关系,后面我们将看到具体的案例。
3.实战

// module1.js
let s=’’;
function proc(){
s=‘123456’;
return s;
}
export {num,inc};// 暴露接口
1
2
3
4
5
6
7
// main.js
import {s,proc} from ‘./module1’;// 引入依赖
console.log(s);//
console.log(proc);// 123456
1
2
3
4
       需要注意的一点是,在使用CommonJS规范时,输出的是值的拷贝,也就是说输出之后,模块内部的变化不会影响输出。但在ES6中是恰好相反的,ES6规范中输出的是值的引用,也就是说模块内部变化会影响输出。从上例可以看到,第一次输出的s为空字符串,调用proc后s被修改,再次输出后s变为处理后的字符串。下面我们再来看一个例子:

//module2.js
export let num=0;
export function inc(){
num++;
}
1
2
3
4
5
//main.js
import {num,inc} from ‘./module2’;
console.log(num);// 0
inc();
console.log(num);// 1
1
2
3
4
5
       我们可以看到,在输出之后内部的变化仍然会反映到输出上。这是因为CommonJS加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而ES6模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成,所以ES6模块是动态引用,不会缓存值,模块里面的变量绑定其所在的模块。
       从上面两个例子可以看到,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

// module3.js
export default function(){
console.log(‘foo’);
}
1
2
3
4
// main.js
import iWant from ‘./module3’;
iWant();// foo
1
2
3
       在引入具有默认输出的模块时,如果该输出是一个匿名函数,import可以给匿名函数重命名。那么我们如何引入第三方的模块呢?

// 在引入之前要先安装第三方库
// main.js
import $ from ‘jquery’;// 引入第三方库
$(‘body’).css(xxx);
1
2
3
4
总结
CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD、CMD解决方案。
AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。
CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。但是依赖SPM打包,模块的加载逻辑偏重。
ES6在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代CommonJS和 CMD规范,成为浏览器和服务器通用的模块化解决方案。
————————————————
版权声明:本文为CSDN博主「五撸超人」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_41631970/article/details/89467548

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值