在最开始学习前端的时候只需要一个js文件就能玩转一个小的练手应用,但是随着自己不断的学习,ajax、jQuery等广泛应用,使得我们的代码量变得巨大,代码变得格外的混乱。现在迫切的需要我们将大段的代码分离开来。
前端最开始并没有像java中package概念以及import那样的引包工具。JavaScript源生代码是在ES6的时候才正式的引入import这个API,来调用其他文件。在这之前也同样出现了很多社区来实现模块化开发。
注意下面会讲历史上面出现的一些类库,有一些现在已经没有人用了,所以建议知道有过就行。
function fn1() {}
function fn2() {}
将函数挂载到全局上,通过函数名就可以直接调用。但是这种方式污染全局,容易发生命名冲突。
var math = {
_addFir: 2,
add: function(addSec) {
return _addFir + addSec;
},
};
对象写法相对来说减少了全局变量,但是一点也不安全。 例如:
math._addFir = 10;
console.log(math.add(2)); // 12
对象内部的变量可以被外面修改。
var math = (function() {
var _addFir = 2;
function add(addSec) {
return _addFir + addSec;
}
return {
add: add,
};
})();
这样就无法修改函数内部的_addFir参数了
_addFir = 10;
console.log(math.add(2)); // 4
var get = (function($) {
var $p = $('input');
function getVal() {
return $p.val();
}
return {
getVal: getVal,
};
})(jQuery);
使用立即执行的方式写模块,当模块非常大的时候,我们就需要将这个模块分成几部分来进行编写。下面将会展示廖雪峰老师所说的放大模式和宽放大模式(我暂时想不出其他的名字,就使用了现有的)。
var module = (function(mod) {
mod.add = function(a, b) {
return a + b;
};
return mod;
})(module);
上面的方式实现了将已存在的对象添加方法,使之放大,但是如果module起初还没有被加载到文件中怎么办,下面就用到了宽放大模式。
var module = (function(mod) {
mod.add = function(a, b) {
return a + b;
};
return mod;
})(module || {});
这样就解决了module模块没有加载出来,报错的问题。
我们最初使用html中的<script>
标签来引入js文件。当项目不断变大以后,我们的项目的依赖也开始变多,就像下面。
<body>
...
<script src="jQuery.js"></script>
<script src="zepto.js"></script>
<script src="iScroll.js"></script>
<script src="math.js"></script>
<script src="dom.js"></script>
...
</body>
大量的script标签排列在我们的html文件中。
缺点
LABjs它是一个文件加载器,使用script和wait实现文件异步和同步加载,解决文件之间的相互依赖,使的文件加载的性能大大提高。有了它我们的html中引脚本文件可以成下面这样。
<script src="LAB.js"></script>
<script>
$LAB
.script('jQuery.js').wait() // .wait是等待此文件的加载完成,当所有的文件都需要依赖jQuery中的api,必须等jQuery文件加载好以后才能调用jQuery
.script('a.js')
.script('b.js')
.script('c.js')
.script('math.js')
// .script(['a.js', 'b.js', 'c.js', 'math.js']) // 同时加载所有的js文件
.wait(function() { // 等所有的js文件加载完成以后,执行这里的代码块
math.add(2, 2);
})
</script>
同时LABjs也可以解决所有文件之间都相互依赖的问题
<script src="LAB.js"></script>
<script>
$LAB
.setOptions({AlwaysPreserveOrder:true}) // 下面需要加载的这些文件之间都相互依赖
.script(['a.js', 'b.js', 'c.js', 'math.js'])
.wait(function() {
math.add(2, 2);
})
YUI用来基于模块的依赖管理。
YUI.add('module1', function(Y) {...}, '1.0.0', requires: []);
其中YUI是全局变量,就像是jQuery;第一个参数是此模块的名字;第二个参数中函数的内容就是此模块的内容;第三个参数是此模块的版本号;第四个参数是此模块需要依赖的模块有哪些。 下面将展示如何使用YUI添加和使用一个模块
// hello.js
YUI.add('hello', function(Y) {
Y.sayHello = function() {
Y.DOM.set(el, 'innerHTML', 'hello!');
}
}, '1.0.0',
requires: ['dom']);
// index.html
<div id="entry"></div>
<script>
YUI().use('hello', function(Y) {
Y.sayHello('entry'); // <div id="entry">hello!</div>
})
</script>
我不想花太多时间在这个上面,所以后面只会写生成模块和使用模块。如果对这个有兴趣可以到:YUI3
the spec does not define a standard library that is useful for building a broader range of applications. 该规范没有定义一个标准库,可用于构建更广泛的应用程序。
上面这段话来自CommonJS官网中的自我定位,它本质上面是一个规范,需要其他的JavaScript类库、框架等自行实现它定义的API。
CommonJS使得JavaScript不仅仅只适用于浏览器,他让js可以编写更多应用程序,如:
commonjs中的模块加载时同步加载,在服务器端,模块存在服务器本地的,加载速度很快。但是,当程序运行在浏览器端的时候要从服务器端去加载模块会导致性能、可用性、调试、跨域等问题,所以commonjs不适用与浏览器端。
node应用程序就是根据CommonJS规范实现的,下面我将直接使用node来讲解CommonJS中module和require两个API。
在node中每一个文件就是一个模块,每个模块中变量、函数、对象、类都是私有的,除非将这些放入global中去。
// math.js
var count1 = 2;
global.count2 = 5;
// use.js
console.log(count1); // count1 is not defined
console.log(count2); // 5
node中有一个Module构造函数(node中的lib/module.js),在node应用程序中每个模块都含有一个Module实例,用来存放此模块的信息。
// Module 构造函数
function Module(id, parent) {
this.id; // String,模块标识,为该模块文件在系统中的绝对路径
this.exports; // Object,模块导出的对象
this.parent; // Object,调用此模块的模块信息
this.filename; // String,模块文件的绝对路径
this.loaded; // Boolean,表示模块是否加载完成
this.children; // Array,此模块调用了的模块
this.path; // Array,此模块加载的路径
}
module.exports中的属性就是模块对外输出的接口。
// math.js
var count = 5;
function add(val) {
return count + val;
}
module.exports = { count, add };
// use.js
var math = require('math.js');
console.log(math.count); // 5
math.add(5); // 10
注意module.exports只会输出对象的自身属性,prototype上面的方法是私有方法
// math.js
function math() {}; // 函数即对象
math.count1 = 5;
math.prototype.count2 = 10;
module.exports = math;
// use.js
var math = require('math.js');
console.log(math); // { [Function: math] count1: 5 }
console.log(math.count2); // undefined
exports是node提供的一个变量,用来指向module.exports的引用,相当于每个node文件前面有一段这样的代码exports = module.exports = something
。(something是一个对象)
// math.js
var count = 5;
function add(val) {
return count + val;
}
exports.count = count;
exports.add = add;
// use.js
var math = require('math.js');
console.log(math.count); // 5
math.add(5); // 10
注意模块最终输出的是module.exports,而不是exports。
// math.js
var count = 5;
function add(val) {
return count + val;
}
module.exports = { count, add }; // 此时module的exports指向另一个对象
exports.count = 10; // exports依旧指向的是最开始module.exports指向的对象something
// use.js
var math = require('math.js');
console.log(math.count); // 注意,这里打印的还是5
math.add(5); // 10
由上面代码可以看出,当module.exports发生改变的时候,exports失效,这就很正常了。如果想让后面的exports的操作能改变输出的话,使exports的指向module.exports新的引用就行了。
// math.js
var count = 5;
function add(val) {
return count + val;
}
exports = module.exports = { count, add };
exports.count = 10;
// use.js
var math = require('math.js');
console.log(math.count); // 10
math.add(5); // 10
在讲exports的最后,提醒大家,想要使用exports对外输出的时候不是对exports赋值。如果大家看了多次还是不懂exports的用法,那就去看下这篇module.exports与exports??关于exports的总结。
下面是来自廖雪峰的require() 源码解读翻译翻译自《Node使用手册》。
当Node 遇到 require(X) 时,按下面的顺序处理。
(1)如果 X 是内置模块(比如 require('http'))
a. 返回该模块。
b. 不再继续执行。
(2)如果 X 以 "./" 或者 "/" 或者 "../" 开头
a. 根据 X 所在的父模块,确定 X 的绝对路径。
b. 将 X 当成文件,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。
X
X.js
X.json
X.node
c. 将 X 当成目录,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。
X/package.json(main字段)
X/index.js
X/index.json
X/index.node
(3)如果 X 不带路径
a. 根据 X 所在的父模块,确定 X 可能的安装目录。
b. 依次在每个目录中,将 X 当成文件名或目录名加载。
(4) 抛出 "not found"
当文件/home/test/use.js
中使用require('math')
,这种情况属于上面的(3)。
首先会确定文件的绝对路径,并依此去寻找每个目录
/home/test/node_modules/math
/home/node_modules/math
/node_modules/math
在寻找每个目录中的文件的时候,node会现将math当成一个文件。当依此寻找到一个以后就会立马返回。
math
math.js
math.json
math.node
把math当成文件并没有找到的时候,就会将math当成文件夹,并去依此寻找他下面的这些文件。
package.json(main字段)
index.js
index.json
index.node
require会按照上面的顺序依次去查询是否含有这个文件,如果找到了就会立马加载此文件,并停止去遍历那些路径。 如果将确定好的绝对路径目录都寻找了一遍没有找到目标文件时,就会抛出一个错误。
node中模块不会被重复加载,node会将加载过的文件名缓存下来,以后再次访问时就不会重复加载模块了。
注意这里缓存的文件名并不是require中的参数,require('math')和require('./node_modules/math')只会去解析一次此模块。
node中的每个模块实例都有一个require方法。
Module.prototype.require = function(path) {
return Module._load(path, this);
}
从上面的代码可以看出,require并不是全局变量,而是模块内部的一个方法。
下面是Module._load的源码
Module._load = function(request, parent, isMain) {
// 计算绝对路径
var filename = Module._resolveFilename(request, parent);
// 第一步:如果有缓存,取出缓存
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
// 第二步:是否为内置模块
if (NativeModule.exists(filename)) {
return NativeModule.require(filename);
}
// 第三步:生成模块实例,存入缓存
var module = new Module(filename, parent);
Module._cache[filename] = module;
// 第四步:加载模块
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
// 第五步:输出模块的exports属性
return module.exports;
};
AMD:异步模块定义规范。模块和模块的依赖可以通过异步加载。前面说了程序运行在浏览器端的时候,如果同步的去加载服务器中模块会导致性能、可用性、调试、跨域等问题。AMD推荐依赖前置,require.js从2.0开始也支持依赖就近了。
AMD规范现在给出了define和require两个全局函数。
define(id, [module], callback);
第一个参数id字符串,指的是这个模块的名字,可选,如果使用了这个参数,加载此模块时填写的模块名应该默认为此id;第二个参数[module],此模块的依赖列表,异步加载这些依赖。第三个参数callback,此模块的所要执行的函数或者对象,如果此模块有依赖的模块,那么callback参数的顺序应该和[module]顺序一致。
如下一个math模块没有依赖
// math.js
define({
add: function(x, y) {
return x + y;
},
});
// 同下
define(function() {
function add(x, y) {
return x + y;
}
return {
a: a,
};
});
math模块需要依赖其他模块
// math.js
define(['other'], function(oth) {
function add(x, y) {
return oth(x, y);
}
return {
add: add
};
});
require.js是基于AMD规范的模块加载器。基本思想是使用define来定义模块,使用require来加载模块。
define([module], callback);
require([module], callback, errCallback);
define和require的前两个参数是一样的,第一个参数是[module]是此模块依赖的模块的加载数组,第二个参数是当依赖模块加载完成后调用的回调函数。require支持第三个参数errCallback, 是处理错误的函数。
首先将require.js文件嵌入网页中。
<script data-main="scripts/main" src="scripts/require.js"></script>
data-main的作用是定义网页的主模块,所以scripts中的main.js是第一个被require的脚本文件。require默认文件扩展名是.js
主模块中一般会依赖其他的文件。
require(['mod1', 'mod2'], function(mod1, mod2) {
...
})
require()加载模块的时候浏览器不会失去响应,它会现将所要依赖的模块准备好,然后依赖前置的方式将模块引用进来。
上面主模块中加载的mod1和mod2,默认是这两个依赖模块和主模块在同一个目录下。
如果mod1和mod2都位于主模块目录中的lib目录下:
require.config({
bathUrl: './lib'
paths: {
mod1: ['mod', 'mod1'],
mod2: 'mod2'
},
})
require([], function)
paths参数指定各个模块的位置。这个位置可以是同一个服务器上的相对位置,也可以是外部网址。可以为每个模块定义多个位置,如果第一个位置加载失败,则加载第二个位置,上面的例子中的mod1的加载中第一个位置加载出错后,会自动加载数组中的第二个地址。当模块的路径指定本地文件路径时,可以省略文件最后的js后缀名。
改变基准目录,本地的模块相对于哪个目录。
此属性帮助require.js来加载非AMD规范的库。(没验证)
require.config({
paths: {
"backbone": "vendor/backbone",
"underscore": "vendor/underscore"
},
shim: {
"backbone": {
deps: [ "underscore" ],
exports: "Backbone"
},
"underscore": {
exports: "_"
}
}
});
CMD规范是用来规定程序在浏览器环境下的模块开发。CMD推崇一个模块就是一个文件,依赖就近。注意:CMD依赖就近并不是在那个时候才开始加载模块,它会事先将模块准备好,依赖就近是引用就近。
CMD提供了一个全局函数define(factory)
,define是用来定义模块用的。它以factory为参数,其中factory可以是函数、对象、字符串。如果factory是对象或者字符串的时候,那表示此模块对外的接口就是这个对象或者字符串。
下面分别定义的一个JSON模块,和字符串模块。
define({ newborn: 'hello' });
define('welcome, newborn!');
当factory是函数的时候,这个函数就是这个模块的构造函数。factory默认会传入三个参数依次是require|exports|module
define(function(require, exports, module) {...});
define(id, [module], factory)同样也可以支持三个参数,和AMD规范中的参数一样,但是当define带有id和[module]参数的时候,就已经不属于CMD规范了
require是factory的默认的参数。它使得在模块内部同步的引用依赖模块。
define(function(require) {
var $ = require('jquery'); // 注意这里是同步执行的
...
})
引用模块是需要时间的,当引用多个模块并分别对这些模块进行调用,如果还是同步的去执行,会消耗很多不必要的时间。
require.async表示的是在模块内部执行异步操作。
define(function(require) {
require.async(['jquery'], function($) {
...
});
require.async(['a'], function(a) {
...
});
})
exports、return、module.exports,这三种方法都行。 exports方法
define(function(require, exports) {
exports.add = function(x, y) {
return x + y;
};
exports.count = 5;
})
return 方法
define(function() {
return {
add: function() {},
count: 5,
}
})
module.exports方法
define(function(require, exports, module) {
module.exports = {
add: function() {},
count: 5,
}
})
注意: exports是module.exports的一个引用,如果给exports进行赋值,不会影响到模块对外接口
define(function(require, exports) {
// 错误用法
exports = {
add: function() {},
count: 5,
}
// 正确用法
module.exports = {
add: function() {},
count: 5,
}
})
这里就不重复讲解这是为什么了,CMD中的exports和module.exports和前面前面讲解使用CommonJS规范的node中exports和module.exports的使用方式一样。但是,注意这里的module和node中的module不是一个东西.
sea.js是CMD规范的最佳实践,在这里也不去讲述了。
想要了解AMD和CMD的区别可以去: JavaSript模块规范 - AMD规范与CMD规范介绍
本篇中前面的LABjs和YUIjs都已经成为历史,个人觉得只需要知道有过就行了,因为篇幅问题,还有很多关于模块化的内容没有写,比如UMD、es6等。
下面是文章中所引用的连接