凉风有信,秋月无边。
亏我思娇的情绪好比度日如年。

JavaScript 模块化入门Ⅰ:理解模块

《JavaScript 模块化入门Ⅰ:理解模块》正文开始,本次阅读大概9分钟。

作为一名JS初学者。假如你听到了一些诸如“模块化构建模块化载入” “WebpackBrowserify” 或者 “AMDCMD”之类的术语,肯定瞬间就凌乱了。

JavaScript的模块化听起来挺深奥,可其实理解它对开发者来说特别实用。

在这篇文章里,我会尽量深入浅出地把这些深奥的术语翻译成浅显易懂的人话(加上一些代码示例)。希望你多少能从中学到点东西。

为了避免长篇大论,整个内容会分为两篇文章,这是第一部分,主要介绍模块化是什么,为什么要使用模块。之后的第二部分会介绍如何打包JS模块,以及各类构建工具。

求解释到底什么是模块化

称职的作家会把他的书分章节和段落;好的程序员会把他的代码分成模块。

就好像书籍的一章,模块仅仅是一坨代码而已。

好的代码模块分割的内容一定是很合理的,便于你增加减少或者修改功能,同时又不会影响整个系统。

为什么要使用模块

模块化可以使你的代码低耦合,功能模块直接不相互影响。我个人认为模块化主要有以下几点好处:

1.可维护性:根据定义,每个模块都是独立的。良好设计的模块会尽量与外部的代码撇清关系,以便于独立对其进行改进和维护。维护一个独立的模块比起一团凌乱的代码来说要轻松很多。

2.命名空间:在JavaScript中,最高级别的函数外定义的变量都是全局变量(这意味着所有人都可以访问到它们)。也正因如此,当一些无关的代码碰巧使用到同名变量的时候,我们就会遇到“命名空间污染”的问题。

这样的问题在我们开发过程中是要极力避免的。

后面的内容里我也会举一些具体的例子来说明这一点。

3.可复用性:现实来讲,在日常工作中我们经常会复制自己之前写过的代码到新项目中。

复制粘贴虽然很快很方便,但难道我们找不到更好的办法了么?要是……有一个可以重复利用的模块岂不妙哉?

如何引入模块

引入模块有很多种方式,这里我们先介绍一些:

模块模式

模块模式一般用来模拟类的概念(因为原生JavaScript并不支持类,虽然最新的ES6里引入了Class不过还不普及)这样我们就能把公有和私有方法还有变量存储在一个对象中——这就和我们在Java或Python里使用类的感觉一样。这样我们就能在公开调用API的同时,仍然在一个闭包范围内封装私有变量和方法。

实现模块模式的方法有很多种,下面的例子是通过匿名闭包函数的方法。(在JavaScript中,函数是创建作用域的唯一方式。)

例1:匿名闭包函数

(function(){
//在函数的作用域中下面的变量是私有的
varmyGrades=[93,95,88,0,55,91];
varaverage=function(){
vartotal=myGrades.reduce(function(accumulator,item){
returnaccumulator+item},0);
returnYouraveragegradeis+total/myGrades.length+.;
}
varfailing=function(){
varfailingGrades=myGrades.filter(function(item){
returnitem70;});
returnYoufailed+failingGrades.length+times.;
}
console.log(failing());
}());
//控制台显示:Youfailed2times.
通过这种构造,我们的匿名函数有了自己的作用域或“闭包”。 这允许我们从父(全局)命名空间隐藏变量。

这种方法的好处在于,你可以在函数内部使用局部变量,而不会意外覆盖同名全局变量,但仍然能够访问到全局变量,如下所示:

varglobal=Hello,Iamaglobalvariable:);
(function(){
//在函数的作用域中下面的变量是私有的
varmyGrades=[93,95,88,0,55,91];
varaverage=function(){
vartotal=myGrades.reduce(function(accumulator,item){
returnaccumulator+item},0);
returnYouraveragegradeis+total/myGrades.length+.;
}
varfailing=function(){
varfailingGrades=myGrades.filter(function(item){
returnitem70;});
returnYoufailed+failingGrades.length+times.;
}
console.log(failing());
console.log(global);
}());
//控制台显示:Youfailed2times.
//控制台显示:Hello,Iamaglobalvariable:)

要注意的是,一定要用括号把匿名函数包起来,以关键词function开头的语句总是会被解释成函数声明(JS中不允许没有命名的函数声明),而加上括号后,内部的代码就会被识别为函数表达式。其实这个也叫作立即执行函数(IIFE)感兴趣的同学可以在这里了解更多

例2:全局引入

另一种比较受欢迎的方法是一些诸如jQuery的库使用的全局引入。和我们刚才举例的匿名闭包函数很相似,只是传入全局变量的方法不同:

(function(globalVariable){
//在函数的作用域中下面的变量是私有的
varprivateFunction=function(){
console.log(Shhhh,thisisprivate!);
}
//通过全局变量设置下列方法的外部访问接口
//与此同时这些方法又都在函数内部
globalVariable.each=function(collection,iterator){
if(Array.isArray(collection)){
for(vari=0;icollection.length;i++){
iterator(collection[i],i,collection);
}
}else{
for(varkeyincollection){
iterator(collection[key],key,collection);
}
}
};
globalVariable.filter=function(collection,test){
varfiltered=[];
globalVariable.each(collection,function(item){
if(test(item)){
filtered.push(item);
}
});
returnfiltered;
};
globalVariable.map=function(collection,iterator){
varmapped=[];
globalUtils.each(collection,function(value,key,collection){
mapped.push(iterator(value));
});
returnmapped;
};
globalVariable.reduce=function(collection,iterator,accumulator){
varstartingValueMissing=accumulator===undefined;
globalVariable.each(collection,function(item){
if(startingValueMissing){
accumulator=item;
startingValueMissing=false;
}else{
accumulator=iterator(accumulator,item);
}
});
returnaccumulator;
};
}(globalVariable));

在这个例子中,globalVariable 是唯一的全局变量。这种方法的好处是可以预先声明好全局变量,让你的代码更加清晰可读。

例3:对象接口

像下面这样,还有一种创建模块的方法是使用独立的对象接口:

varmyGradesCalculate=(function(){
//在函数的作用域中下面的变量是私有的
varmyGrades=[93,95,88,0,55,91];
//通过接口在外部访问下列方法
//与此同时这些方法又都在函数内部
return{
average:function(){
vartotal=myGrades.reduce(function(accumulator,item){
returnaccumulator+item;
},0);
returnYouraveragegradeis+total/myGrades.length+.;
},
failing:function(){
varfailingGrades=myGrades.filter(function(item){
returnitem70;
});
returnYoufailed+failingGrades.length+times.;
}
}
})();
myGradesCalculate.failing();//Youfailed2times.
myGradesCalculate.average();//Youraveragegradeis70.33333333333333.

例4:揭示模块模式 Revealing module pattern

这和我们之前的实现方法非常相近,除了它会确保,在所有的变量和方法暴露之前都会保持私有:

varmyGradesCalculate=(function(){
//在函数的作用域中下面的变量是私有的
varmyGrades=[93,95,88,0,55,91];
varaverage=function(){
vartotal=myGrades.reduce(function(accumulator,item){
returnaccumulator+item;
},0);
returnYouraveragegradeis+total/myGrades.length+.;
};
varfailing=function(){
varfailingGrades=myGrades.filter(function(item){
returnitem70;
});
returnYoufailed+failingGrades.length+times.;
};
//将公有指针指向私有方法
return{
average:average,
failing:failing
}
})();
myGradesCalculate.failing();//Youfailed2times.
myGradesCalculate.average();//Youraveragegradeis70.33333333333333.

到这里,其实我们只聊了模块模式的冰山一角。

CommonJS AMD

上述的所有解决方案都有一个共同点:使用单个全局变量来把所有的代码包含在一个函数内,由此来创建私有的命名空间和闭包作用域。

虽然每种方法都比较有效,但也都有各自的短板。

有一点,作为开发者,你必须清楚地了解引入依赖文件的正确顺序。就拿Backbone.js来举个例子,想要使用Backbone就必须在你的页面里引入Backbone的源文件。

然而Backbone又依赖 Underscore.js,所以Backbone的引入必须在其之后。

而在工作中,这些依赖管理经常会成为让人头疼的问题。

另外一点,这些方法也有可能引起命名空间冲突。举个例子,要是你碰巧写了俩重名的模块怎么办?或者你同时需要一个模块的两个版本时该怎么办?

难道就没有不通过全局作用域来实现的模块方法么?

当然是有的。

接下来介绍两种广受欢迎的解决方案:CommonJS 和 AMD.

CommonJS

CommonJS 扩展了JavaScript声明模块的API.

CommonJS模块可以很方便得将某个对象导出,让他们能够被其他模块通过 require 语句来引入。要是你写过 Node.js 应该很熟悉这些语法。

通过CommonJS,每个JS文件独立地存储它模块的内容(就像一个被括起来的闭包一样)。在这种作用域中,我们通过 module.exports 语句来导出对象为模块,再通过 require 语句来引入。

还是举个直观的例子吧:

functionmyModule(){
this.hello=function(){
returnhello!;
}
this.goodbye=function(){
returngoodbye!;
}
}
module.exports=myModule;

通过指定导出的对象名称,CommonJS模块系统可以识别在其他文件引入这个模块时应该如何解释。

然后在某个人想要调用 myMoudle 的时候,只需要 require 一下:

varmyModule=require(myModule);
varmyModuleInstance=newmyModule();
myModuleInstance.hello();//hello!
myModuleInstance.goodbye();//goodbye!

这种实现比起模块模式有两点好处:

避免全局命名空间污染

明确代码之间的依赖关系

并且这种书写方式也非常舒服友好,我自己很喜欢。

需要注意的一点是,CommonJS以服务器优先的方式来同步载入模块,假使我们引入三个模块的话,他们会一个个地被载入。

它在服务器端用起来很爽,可是在浏览器里就不会那么高效了。毕竟读取网络的文件要比本地耗费更多时间。只要它还在读取模块,浏览器载入的页面就会一直卡着不动。(在下一篇第二部分的教程里我们会讨论如何解决这个问题)

AMD

CommonJS已经挺不错了,但假使我们想要实现异步加载模块该怎么办?答案就是Asynchronous Module Definition(异步模块定义规范),简称AMD.

通过AMD载入模块的代码一般这么写:

define([myModule,myOtherModule],function(myModule,myOtherModule){
console.log(myModule.hello());});

这里我们使用 define 方法,第一个参数是依赖的模块,这些模块都会在后台无阻塞地加载,第二个参数则作为加载完毕的回调函数。

回调函数将会使用载入的模块作为参数。在这个例子里就是 myMoudle 和 myOtherModule.最后,这些模块本身也需要通过 define 关键词来定义。

拿 myModule 来举个例子:

define([],function(){
return{
hello:function(){
console.log(hello);
},
goodbye:function(){
console.log(goodbye);
}
};
});
重申一下,不像CommonJS,AMD是优先浏览器的一种异步载入模块的解决方案。(记得,很多人认为一个个地载入小文件是很低效的,我们将在下一篇文章理介绍如何打包模块)

除了异步加载以外,AMD的另一个优点是你可以在模块里使用对象、函数、构造函数、字符串、JSON或者别的数据类型,而CommonJS只支持对象。

再补充一点,AMD不支持Node里的一些诸如 IO,文件系统等其他服务器端的功能。另外语法上写起来也比CommonJS麻烦一些。

UMD

在一些同时需要AMD和CommonJS功能的项目中,你需要使用另一种规范:Universal Module Definition(通用模块定义规范)。

UMD创造了一种同时使用两种规范的方法,并且也支持全局变量定义。所以UMD的模块可以同时在客户端和服务端使用。

下面是一个解释其功能的例子:

(function(root,factory){
if(typeofdefine===functiondefine.amd){
//AMD
define([myModule,myOtherModule],factory);
}elseif(typeofexports===object){
//CommonJS
module.exports=factory(require(myModule),require(myOtherModule));
}else{
//Browserglobals(Note:rootiswindow)
root.returnExports=factory(root.myModule,root.myOtherModule);
}
}(this,function(myModule,myOtherModule){
//Methods
functionnotHelloOrGoodbye(){};//Aprivatemethod
functionhello(){};//Apublicmethodbecauseitsreturned(seebelow)
functiongoodbye(){};//Apublicmethodbecauseitsreturned(seebelow)
//Exposedpublicmethods
return{
hello:hello,
goodbye:goodbye
}
}));

更多有关UMD的例子请看其Github上的官方repo.

原生JS

希望你坚持读到了现在,我们最后再介绍一种定义模块的方式。

你可能注意到了,上述的这几种方法都不是JS原生支持的。要么是通过模块模式来模拟,要么是使用CommonJS或AMD.

幸运的是在JS的最新规范ECMAScript 6 (ES6)中,引入了模块功能。

ES6 的模块功能汲取了CommonJS 和 AMD 的优点,拥有简洁的语法并支持异步加载,并且还有其他诸多更好的支持。

我最喜欢的ES6 模块功能的特性是,导入是实时只读的。(CommonJS 只是相当于把导出的代码复制过来)。

来看例子:

//lib/counter.js
varcounter=1;
functionincrement(){
counter++;
}
functiondecrement(){
counter--;
}
module.exports={
counter:counter,
increment:increment,
decrement:decrement
};
//src/main.js
varcounter=require(../../lib/counter);
counter.increment();
console.log(counter.counter);//1

上面这个例子中,我们一共创建了两份模块的实例,一个在导出的时候,一个在引入的时候。

在 main.js 当中的实例是和原本模块完全不相干的。这也就解释了为什么调用了 counter.increment() 之后仍然返回1。因为我们引入的 counter 变量和模块里的是两个不同的实例。

所以调用 counter.increment() 方法只会改变模块中的 counter .想要修改引入的 counter 只有手动一下啦:

counter.counter++;console.log(counter.counter);//2

而通过 import 语句,可以引入实时只读的模块:

//lib/counter.js
exportletcounter=1;
exportfunctionincrement(){
counter++;
}
exportfunctiondecrement(){
counter--;
}
//src/main.js
import*ascounterfrom../../counter;
console.log(counter.counter);//1
counter.increment();
console.log(counter.counter);//2

这看起来很酷不是么?这样就实现了我们把模块分隔在不同的文件里,需要的时候又可以合并在一起而且不影响它的功能。

赞(268)
【声明】:本博客不参与任何交易,也非中介,仅记录个人感兴趣的内容,内容均不作直接、间接、法定、约定的保证。访问本博客请务必遵守有关互联网的相关法律、规定与规则。一旦您访问本博客,即表示您已经知晓并接受了此声明通告。本博客资源仅供个人学习交流,请于下载后24小时内删除,不允许用于商业用途,否则法律问题自行承担。如果本文导致的版权问题以及内容纠错问题请联系站长QQ:1004619 | 点此给我发送邮件
本文标题:JavaScript 模块化入门Ⅰ:理解模块
本文地址:https://www.1004619.com/nn/javascriptmkhrmljmk.html