Javascript不是一种模块化编程语言,它不支持"类"(class),更遑论"模块"(module),然而模块化编程,已经成为一个迫切的需求。
* 模块化的写法
基本写法
一:原始写法
模块就是实现特定功能的一组方法,只要把不同的函数(以及记录状态的变量)简单地放在一起,就算是一个模块。
function f1() {...}
function f2() {...}
上面的函数f1()和f2(),组成一个模块。使用的时候,直接调用就行了。
这种做法的缺点很明显:
- "污染"了全局变量;
- 无法保证不与其他模块发生变量名冲突;
- 而且模块成员之间看不出直接关系。
二:对象写法
为了解决上面的缺点,可以把模块写成一个对象,所有的模块成员都放到这个对象里面.
var module = new Object {
_count: 0;
f1: function(){
// some code
}
f2: function() {
// some code
}
}
这样,我们调用方式就变成了
module.f1();
这么写还有缺点:暴露了所有的模块成员,内部的状态可以被外部改变。比如
module._count = 5;
三:立即执行函数写法
使用【立即执行函数】(Immediately-Invoked Function Expression,IIFE),可以达到不暴露成员变量的目的
var module = (function() {
var _count = 0;
var f1 = function(){
// some code
};
var f2 = function(){
// some code
};
return {
f1: f1,
f2: f2
}
})();
console.log(module._count); // undefined
这样,外部代码无法读取内部的_count变量
放大模式
如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用"放大模式"(augmentation)。
为module1模块添加了一个新方法m3(),然后返回新的module1模块
var module = function(mod){
var mod.f3 = function(){
// some code
}
return mod;
})(module);
在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上一节的写法,第一个执行的部分有可能加载一个不存在空对象,为了更严谨一点,这时就要采用"宽放大模式"。
var module = (function(mod){
// some code
return mod;
})(window.module || {});
这样,立即执行函数"的参数就可以是空对象
输入全局变量
- 独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。
- 为了在模块内部调用全局变量,必须显式地将其他变量输入模块
var module = (function($, YAHOO){
// some code
})(jQuery,YAHOO);
上面的module1模块需要使用jQuery库和YUI库,就把这两个库(其实是两个模块)当作参数输入module1。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。
* 模块化的规范
为什么模块很重要?因为有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。但是,这样做有一个前提,那就是大家必须以同样的方式编写模块,否则你有你的写法,我有我的写法,岂不是乱了套!
CommonJs规范
2009年,美国程序员Ryan Dahl创造了 node.js 项目,将javascript语言用于服务器端编程。这标志"Javascript模块化编程"正式诞生.老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;但是在服务器端,一定要有模块,与操作系统和其他应用程序互动,否则根本没法编程。
node.js的 模块系统 ,就是参照 CommonJS 规范实现的。在CommonJS中,有一个全局性方法require(),用于加载模块。假定有一个数学模块math.js
,就可以像下面这样加载。
var math = require('math');
然后就可以调用模块定义的方法:
math.add(2, 3);
require()
就是用于加载模块的。
AMD规范
AMD 是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义",它采用异步方式加载模块。
模块的加载不影响它后面语句的运行。我们将“后面的语句”分两部分:
- 与该模块无关的语句,受异步模块定义,不受模块加载的影响;
- 所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
AMD采用 require() 加载模块,他有两个参数:
require([modules], callback);
第一个参数 [modules] 是一个数组,里面的成员是要加载的模块,第二个参数是callback, 表示加载之后的回调函数
所以,我们的模块化代码书写方式按照AMD规范就变成如下形式:
require([math], function(math){
math.add(2, 3)
})
math.add()与math模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD比较适合浏览器环境。
* 模块化的使用 - require.js
一: 为什么要用 require.js
最早的时候,所有Javascript代码都写在一个文件里面,只要加载这一个文件就够了。后来,代码越来越多,一个文件不够了,必须分成多个文件,依次加载。相信很多小伙伴都这么写过:
<html>
<head>
...
</head>
<body>
...
<script src="jquery.js"></script>
<script src="base.js"></script>
<script src="core.js"></script>
<script src="bootstrap.js"></script>
<script src="component.js"></script>
<script src="dialog.js"></script>
<script src="event.js"></script>
</body>
</html>
这段代码依次加载多个js文件。这样的写法有很大的缺点:
- 加载的时候,浏览器会停止网页渲染,加载文件越多,网页失去响应的时间就会越长;
- 由于js文件之间存在依赖关系,因此必须严格保证加载顺序(比如上例的jquery.js要在bootstrap.js的前面),依赖性最大的模块一定要放到最后加载,当依赖关系很复杂的时候,代码的编写和维护都会变得困难。
模块化 require.Js的诞生就是为了解决这两个问题:
- 实现js文件的异步加载,避免浏览器假死
- 管理模块之间的依赖,便于模块的维护
二: require.js 的加载
在官网下载最新版本,将其放在js子目录下,index.html文件引入
<script src="js/require.js"></script>
加载这个问价同样会面临页面假死的现象,所以,我们通常这么做:
- 将加载语句放在网页(
<body>
)后面加载; - 增加
defer anysc="true"
属性,如下
<script src="js/require.js" defer async="true"></script>
async属性表明这个文件需要异步加载,避免网页失去响应。IE不支持这个属性,只支持defer,所以把defer也写上。
三:主模块的加载与写法
require.js已经加载完成,下一步就是要加载我们自己的代码。假如文件为main.js
,存放在js子目录下,我们用data-main
属性引入:
<script src="js/require.js" data-main="js/main"></script>
这里的 main.js
,我们称之为“主模块”;意思就是整个网页的入口,它有点像C语言的main()函数,所有代码都从这儿开始运行。
如果我们的代码不依赖任何其他模块,那么可以直接写入javascript代码。
// main.js
alert("load success");
但如果真这样,就没必要使用require.js了。真正常见的情况是,主模块依赖于其他模块,这时就要使用AMD规范定义的的require()
函数。试写一个如下:
// main.js
require(['moduleA', 'moduleB', 'moduleC'], function(moduleA, moduleB, moduleC){
// some code
})
这样,在主模块main.js
中,依赖了三个模块 moduleA,moduleB,moduleC
,当这些模块加载完成之后,调起回调函数,我们将这些模块以参数的形式传入回调函数,从而在回调函数中就可以使用这些模块了。
require()
异步加载moduleA,moduleB
和moduleC
,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。
一个实例
假定主模块依赖jquery
、underscore
和backbone
这三个模块,main.js就可以这样写:
// main.js
require(['jquery', 'underscore', 'backbone'], function($, _, Backbone){
// some code
})
require.js
会先加载jQuery、underscore
和backbone
,然后再运行回调函数。主模块的代码就写在回调函数中。
四:模块的加载
1. 默认加载行为
我们引入jquery
、underscore
和backbone
之后,require.js
会默认为在与main.js
的同级目录下有jquery.js
、underscore.js
和backbone.js
这几个文件,然后自动加载。如果找不到,就会加载失败,引发异常。然而我们常引入且使用的是.min.js
文件,那么就需要我们自定义。
2. 自定义加载行为
使用require.config()
方法,我们可以对模块的加载行为进行自定义。require.config()
就写在主模块(main.js
)的头部。参数就是一个对象,这个对象的paths
属性指定各个模块的加载路径。另外,require.config()
提供了baseUrl
来索引文件,默认的就是main.js
所在路径。
- 默认以
main.js
的相对路径baseUrl
加载
// main.js 头部
require.config({
paths: {
"jquery": "juery.min",
"underscore": "lib/underscore.min",
"backbone": "lib/backbone.min"
}
});
在上方的代码中,jquery.min,js
的路径与main.js
在同一个目录(js子目录),underscore.min.js
和backbone.min.js
则在js目录的子目录lib
中。
- 直接修改基目录
baseUrl
// main.js 头部
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "../juery.min",
"underscore": "underscore.min",
"backbone": "backbone.min"
}
});
- 如果某个模块在另一台主机上,也可以直接指定它的网址
// main.js 头部
require.config({
paths: {
"jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min"
}
})
require.js要求,每个模块是一个单独的js文件。这样的话,如果加载多个模块,就会发出多次HTTP请求,会影响网页的加载速度。因此,require.js提供了一个 优化工具,当模块部署完毕以后,可以用这个工具将多个模块合并在一个文件中,减少HTTP请求数。但其实这种我们也很少用,了解就行。
五:AMD模块的写法
require.js加载的模块,采用AMD规范。具体来说,就是模块必须采用特定的define()函数来定义。
- 新增一个模块,它不依赖其他模块,那么可以直接在define()函数中定义
// math.js
define(function(){
var add = function(x, y){
return x + y;
}
return {
add: add
};
});
使用加载方式如下:
require(['math'],function(math){
alert(math.add(1, 2));
});
- 如果新增的模块需要依赖其他模块
define(['myLib'], function(myLib){
function f1(){
myLib.doSomething();
}
return {
f1: f1
}
})
当require()
函数加上上面这个模块的时候,就会先加载myLib.js
文件
六:加载非规范的模块
理论上,require.js
加载的模块,必须是按照AMD规范、用define()
函数定义的模块,但是实际上,虽然已经有一部分流行的函数库(比如jQuery)符合AMD规范,更多的库并不符合。
加载这样的库,需要先用require.config()
方法定义它们的一些特性。在本文中提到的underscore
和backbone
这两个库,就没有采用AMD来编写,使用时我们必须先定义它们的特性:
require.config({
shim: {
'underscore': {
exports: '_'
},
'backbone': {
deps: ['underscore', 'jquery'],
exports: 'Backbone'
}
}
})
require.config()
接受一个配置对象,这个对象除了有前面说过的paths
属性之外,还有一个shim
属性,专门用来配置不兼容的模块。
每个模块都至少要定义:
- exports值(输出的变量名),定义这个模块外部调用时的名称;
- deps数组,表明该模块的依赖列表。 当然,如果没有依赖可以省略。
如下,我们可以这样定义一个jquery
的依赖模块
require.config({
shim: {
'jquery.scroll': {
deps: ['jquery'],
exports: 'jQuery.fn.scroll'
}
}
})
七:require.js的插件
require.js
还提供一系列 插件,实现一些特定的功能。
例如:domready
插件,可以让回调函数在页面DOM结构加载完成后再运行。
require(['domready!'], function(doc){
// called once the DOM is ready
})
text
和image
插件,则是允许require.js
加载文本和图片文件。
require(['text!review.txt', 'image!cat.jpg'], function(review, cat){
console.log(review);
document.body.appendChild(cat);
})
类似的插件还有json
和mdown
,用于加载json
文件和markdown
文件。