(一)JavaScript模块化
什么是模块:模块就是用于实现特定功能的一组方法;
I.只要把不同的函数(以及记录状态的变量)简单地放在一起,就算是一个模块;
function m1(){
//......
}
function m2(){
//......
}
上面的函数,m1,m2就组成了一个模块。使用的时候直接调用就行了。
缺点:1.污染全局变量,无法保证不与其他模块命名冲突;2.模块成员之间看不出直接关系
II.对象的写法:
var module1=new Object({
_count:0;
m1:function(){
//...
};
m2:function(){
//...
};
});
上面的函数m1()和m2(),都封装在module1对象里。使用的时候,就是调用这个对象的属性。
module1.m1();
缺点: 这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值。
module1._count=5;
III.使用匿名函数(立即执行函数写法)[这就是用的闭包的思想]
可以达到不暴露私有成员的目的。
var module1=(function(){
var _count=0;
var m1=function(){
//......
};
var m2=function(){
//......
} ;
return {m1:m1;m2:m2}
})()
module1.m1()
VI.放大模式
如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用"放大模式"(augmentation)
var module1=(function(mod){
mod.m3=function(){
//......
}
return mod;
})(module1)
上面的代码为module1模块添加了一个新方法m3(),然后返回新的module1模块。
V.宽放大模式
在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上一节的写法,第一个执行的部分有可能加载一个不存在空对象,这时就要采用"宽放大模式"。
var module1 = ( function (mod){
//...
return mod;
})(window.module1 || {});
与"放大模式"相比,"宽放大模式"就是"立即执行函数"的参数可以是空对象。
六、输入全局变量
独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。
为了在模块内部调用全局变量,必须显式地将其他变量传入模块。
var module1=(function($,YAHOO){
//...
})(Jquery,YAHOO)
上面的module1模块需要使用jQuery库和YUI库,就把这两个库(其实是两个模块)当作参数输入module1。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。
以上是我们一般需要写的模块!
在一个JavaScript文件中可以写很多个闭包!
但是模块,指的就是单独放在一个文件中用一个匿名立即执行函数包起来的。例如:
模块的特点:1.独立性:一个模块单独在一个js文件中;
2.封闭性:用匿名立即执行函数包围;
3.规范性:模块分为规范性模块和非规范性模块。规范性模块就是AMD规范模块;
(二)AMD规范异步模块定义
上面介绍了JavaScript模块的基本写法,这里介绍如何规范地定义和使用模块;模块分为:1.AMD规范模块
2.非规范模块
其中,AMD规范模块就是符合AMD模式的模块,即用define定义的模块。
要想理解模块并正确的使用模块,那么我们需要首先学习一下require.js。
I.什么是require.js?
require.js就是用js写的一个模块加载器。这个模块加载器,对外提供了三个变量或者称之为方法:define,require,require.js;
1.define:创建AMD规范的模块。该函数用户创建模块。每个模块拥有一个唯一的模块ID,它被用于RequireJS的运行时函数,define函数是一个全局函数,不需要使用requirejs命名空间.
例如:
(1).创建一个math模块
(2).在main.js中使用这个模块
分析: 这里先创建一个math.js文件,然后在这个文件里面在define(callback)方法中传入要创建模块(注:这里的回调函数只要是一个匿名函数。)
然后,在需要调用这个math模块的地方,用require方法加载即require(['math'],function(math){
math.add()....
})
一般define()有三个参数:
第一个参数是name:即定义模块外部引用是的名字或者符号;如果没有这个参数,如上例,则使用文件的名字(math);
如果有name,则需要在使用这个模块之前调用require的config方法去配置路径。即:配置jq这个模块的路径;
第二个参数就是这个模块所依赖的模块。是一个数组;
第三个参数是匿名回调函数,即这个模块的代码;
【define一般我们自己不怎么用,都是写库(模块)的人使用define去写AMD规范模块】
2.require()– 该函数用于读取依赖。同样它是一个全局函数,不需要使用requirejs命名空间.【这个是引用模块。一般我们要使用库,都可以使用这个来引入库】
主要用于加载模块:
有两个参数:第一个参数是一个数组,这个数组存储着所依赖的模块,第二个参数是一个回调函数。
例如:我们写的js需要引用jQuery等模块,则可以这么写
require(['jqeury'],function($){
//这里是我们自己写的代码
})
3. require.config()– 该函数用于配置RequireJS【写在主模块的头部,配置好了才能去加载模块】
config是require函数对象的一个方法,主要用来配置模块
config方法有一个参数,就是一个对象。这个对象有几个属性:baseurl,paths,shim
paths:引入个模块的路径,例如:‘jq’:'js/jquery-3.6.0';jq就是外部引用模块的一个代表或者是模块的名字,后面的属性值就是这个模块的路径。
shim:对于非AMD规范的模块,我们就需要用shim属性规定这些模块。
shim对象属性{
"模块一":{
deps:['模块'];//该模块所依赖的模块
export:‘_’;//这个属性的值是和widow对象中,该模块所对应的属性。
}
}
例如:我们没有用define定义一个模块:
在使用这个模块的时候,需要这么使用:
分析总结:1.没有通过define定义的模块math,其实就是将一个由方法组成的对象赋值给变量math,math变量是widow对象的一个属性:
可能有人就疑惑了,既然math是widow对象的一个属性,那么直接用不就行了还需要一些乱七八糟的过程干嘛?一会config,一会又require的?(或许只有我会这么想)
math不是摆平无故的成为widow对象的属性,是执行了math.js之后,才称为widow的属性。故而,需要将这个math.js加载。那么加载一方面可以通过<script>标签,另一方面就是需要使用require.js实现加载!为了能够正确地被加载,就需要在require.config()中进行配置。config是为了正确地让require去加载模块。
模块加载了,我们就可以在require的回调函数中使用这些模块,即调用这些模块的方法来实现我们的开发 。其实,无论什么框架(模块),如jQuery等都是通过JavaScript实现的一些方法库即模块。 这些库是为什么能够在其他模块中使用呢:
主要方面:模块都是javascript写的一组方法。这些方法被匿名函数包含着,最后返回给引用匿名函数的一个对象。而这个对象是window的一个属性,而window属性在任何一个JavaScript中都可以被调用,故而就可以实现在一个模块中可以引用另一个模块。
附加:先想一想,为什么模块很重要?
因为有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。
模块的使用方法:
1.直接在页面的script标签中加载各种模块。例如:
这里6.js的依赖性最强,必须等前面5个都加载完了之后再加载这个模块。比如,这里我们假设6.js就是我们自己写的a.js文件,1.js是jQuery.js;2.bootscapt.js;3.YOOTOO.js;4...;5...;这里,这些模块都是用JavaScript写的一些方法(故而称为模块:能够完成特定功能的一组方法)。故而,在6.js中,就是去调用这些方法从而实现页面的功能【这些方法都是编写者封装好的,一般只给我们提供api】。虽然这些模块都是通过JavaScript写的方法,但是其模块的用法各方面还是不一样的,模块的名称也不一样。
2.使用require方法加载各模块(注:这里,require本身也是一个模块)
require的用法:只需要在页面中加载一个require模块就可以了。
使用模块这样做有一个前提,那就是大家必须以同样的方式编写模块,否则你有你的写法,我有我的写法,岂不是乱了套!考虑到Javascript模块现在还没有官方规范,这一点就更重要了。
目前,通行的Javascript模块规范共有两种:CommonJS和AMD。我主要介绍AMD,但是要先从CommonJS讲起。
I.CommonJS
node.js的模块系统,就是参照CommonJS规范实现的。在CommonJS中,有一个全局性方法require(),用于加载模块。假定有一个数学模块math.js,就可以像下面这样加载。
var math=require('math');
然后就可以调用模块提供的方法:
var math=require('math');
math. add(2,3) //5
我们在这里只要知道,require()用于加载模块就行了。
II.AMD
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:
第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。如果将前面的代码改写成AMD形式,就是下面这样:
math.add()与math模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD比较适合浏览器环境。
目前,主要有两个Javascript库实现了AMD规范:require.js和curl.js。本系列的第三部分,将通过介绍require.js,进一步讲解AMD的用法,以及如何将模块化编程投入实战。
(三)require.js的用法 参考网址:http://www.ruanyifeng.com/blog/2012/11/require_js.html
最早的时候,所有Javascript代码都写在一个文件里面,只要加载这一个文件就够了【注:所有的script标签引入的js文件,都可以写在一个JavaScript文件中,比如下面的6个模块,可以写在一个文件中,比如1.js写在最上面,接着2.js,...最后是6.js.代码在执行的时候从上到下依次加载执行,比如当用户点击一个元素的时候,就调用上面1——6之间对应的方法。】。后来,代码越来越多,一个文件不够了,必须分成多个文件,依次加载。下面的网页代码,相信很多人都见过。
这段代码依次加载多个js文件。(这和写在一个js文件里面从上到下加载一样的)
这样的写法有很大的缺点:首先,加载的时候,浏览器会停止网页渲染,加载文件越多,网页失去响应的时间就会越长;
其次,由于js文件之间存在依赖关系,因此必须严格保证加载顺序(比如上例的1.js要在2.js的前面),依赖性最大的模块一定要放到最后加载,当依赖关系很复杂的时候,代码的编写和维护都会变得困难。(比如,jQuery的引入必须在JavaScript的前面引入)
require.js的诞生,就是为了解决这两个问题:
(1)实现js文件的异步加载,避免网页失去响应【用require引入的各模块是异步加载的,不需要按照顺序等一个加载完了,再去加载另一个模块。切记:不是因为在页面中只有一个require.js,其余的js文件都在主模块中引入!!!因为我们加载完require.js之后,马上就去加载data-main中的js文件,这个文件中使用require方法加载各模块,这不就跟页面中有script标签因为很多的模块时一样的了?主要就是因为用require方法加载的模块可以异步加载,故而节约了时间,从而提高了效率】;
(2)管理模块之间的依赖性,便于代码的编写和维护。【比如主模块中代码一般写在require的回调函数中,即依赖模块加载好了之后再去执行回调函数】
(1)require.js的加载
使用require.js的第一步,是先去官方网站下载最新版本。
下载后,假定把它放在js子目录下面,就可以加载了。
async属性表明这个文件需要异步加载,避免网页失去响应。IE不支持这个属性,只支持defer,所以把defer也写上。
加载require.js以后,下一步就要加载我们自己的代码了。假定我们自己的代码文件是main.js,也放在js目录下面。那么,只需要写成下面这样就行了:
data-main属性的作用是,指定网页程序的主模块。在上例中,就是js目录下面的main.js,这个文件会第一个被require.js加载。由于require.js默认的文件后缀名是js,所以可以把main.js简写成main。
(2)主模块的写法
上一节的main.js,我把它称为"主模块",意思是整个网页的入口代码。它有点像C语言的main()函数,所有代码都从这儿开始运行。
下面就来看,怎么写main.js。
如果我们的代码不依赖任何其他模块,那么可以直接写入javascript代码。
但这样的话,就没必要使用require.js了。真正常见的情况是,主模块依赖于其他模块,这时就要使用AMD规范定义的的require()函数。
require()函数接受两个参数。第一个参数是一个数组,表示所依赖的模块,上例就是['moduleA', 'moduleB', 'moduleC'],即主模块依赖这三个模块;第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块。
require()异步加载moduleA,moduleB和moduleC,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。
下面,我们看一个实际的例子。
假定主模块依赖jquery、underscore和backbone这三个模块,main.js就可以这样写:
require.js会先加载jQuery、underscore和backbone,然后再运行回调函数。主模块的代码就写在回调函数中。
例如,我们通过require加载jQuery这个插件模块:
1.首先,这里,jQuery.js文件夹和main.js还有require.js在同一目录中:【require加载模块和直接在script标签中加载模块还是有相似之处:都需要URL地址;不同之处:script直接就根据url地址请求加载;而require需要一方面是请求加载,另一方面需要将对应的模块作为参数传递,并且在回调函数中使用这些作为参数传进来的模块】
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script src="js/requirejs.js" data-main="js/main"></script> </head> <body> <p> 这里测试require.js的用法 </p> </body> </html>
main.js中:
require.config({
// jQuery:'/js/jquery-3.6.0'
});
require(['jquery'],function ($) {
$(function () {
$("p").click(function () {
$(this).slideToggle();
});
});
});
可以正确使用jQuery模块了。
上例中,jQuery文件和main.js在同一目录中,那么,如果不在同一目录中,require去哪里加载jQuery.js文件呢?这就涉及到路径的问题
(3)模块的加载
上一节最后的示例中,主模块的依赖模块是['jquery', 'underscore', 'backbone']。默认情况下,require.js假定这三个模块与main.js在同一个目录,文件名分别为jquery.js,underscore.js和backbone.js,然后自动加载。
使用require.config()方法,我们可以对模块的加载行为进行自定义。require.config()就写在主模块(main.js)的头部。参数就是一个对象{},这个对象中包括几个属性:
1.paths:[这里的paths中的属性值也不能带有.js或者.min]
这个对象的paths属性指定各个模块的加载路径:
上面的代码给出了三个模块的文件名,路径默认与main.js在同一个目录(js子目录)。如果这些模块在其他目录,比如js/lib目录,则有两种写法。一种是逐一指定路径。
2.baseUrl
如果某个模块在另一台主机上,也可以直接指定它的网址,比如:
require.js要求,每个模块是一个单独的js文件。这样的话,如果加载多个模块,就会发出多次HTTP请求,会影响网页的加载速度。因此,require.js提供了一个优化工具,当模块部署完毕以后,可以用这个工具将多个模块合并在一个文件中,减少HTTP请求数。
3.shim
require.config()接受一个配置对象,这个对象除了有前面说过的paths属性之外,还有一个shim属性,专门用来配置不兼容的模块。具体来说,每个模块要定义(1)exports值(输出的变量名),表明这个模块外部调用时的名称;(2)deps数组,表明该模块的依赖性。
五、AMD模块的写法
require.js加载的模块,采用AMD规范。也就是说,模块必须按照AMD的规定来写。
具体来说,就是模块必须采用特定的define()函数来定义。如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。
假定现在有一个math.js文件,它定义了一个math模块。那么,math.js就要这样写:
如果这个模块还依赖其他模块,那么define()函数的第一个参数,必须是一个数组,指明该模块的依赖性。
当require()函数加载上面这个模块的时候,就会先加载myLib.js文件。
六、加载非规范的模块
理论上,require.js加载的模块,必须是按照AMD规范、用define()函数定义的模块。但是实际上,虽然已经有一部分流行的函数库(比如jQuery)符合AMD规范,更多的库并不符合。那么,require.js是否能够加载非规范的模块呢?
回答是可以的。
这样的模块在用require()加载之前,要先用require.config()方法,定义它们的一些特征。
举例来说,underscore和backbone这两个库,都没有采用AMD规范编写。如果要加载它们的话,必须先定义它们的特征。
require.config()接受一个配置对象,这个对象除了有前面说过的paths属性之外,还有一个shim属性,专门用来配置不兼容的模块。具体来说,每个模块要定义(1)exports值(输出的变量名),表明这个模块外部调用时的名称;(2)deps数组,表明该模块的依赖性。