一、为什么会有模块化
1. 当一个项目开发的越来越复杂的时候,会遇到一些问题,比如:
-
命名冲突:当项目由团队进行协作开发的时候,不同开发人员的变量和函数命名可能相同;即使是一个开发,当开发周期比较长的时候,也有可能会忘记之前使用了什么变量,从而导致重复命名,导致命名冲突。
-
文件依赖:代码重用时,引入js文件的数目可能少了,或者引入的顺序不对,比如使用boostrap的时候,需要引入jQuery,并且jQuery的文件必须要比boostrap的js文件先引入。
2. 当使用模块化开发的时候可以避免以上的问题,并且让开发的效率变高,以及方便后期的维护:
-
提升开发效率:代码方便重用,别人开发的模块直接拿过来就可以使用,不需要重复开发法类似的功能。
-
方便后期维护:代码方便重用,别人开发的模块直接拿过来就可以使用,不需要重复开发法类似的功能。
所以总结来说,在生产角度,模块化开发是一种生产方式,这种方式生产效率高,维护成本低。从软件开发角度来说,模块化开发是一种开发模式,写代码的一种方式,开发效率高,方便后期维护。
二、模块化开发的演变过程
1. 全局函数
function add(a , b) {
return parseFloat(a) + parseFloat(b);
}
function substract(a ,b) {}
function multiply(a ,b) {}
function divide(a ,b) {}
在早期的开发过程中就是将重复的代码封装到函数中,再将一系列的函数放到一个文件中,这种情况下全局函数的方式只能认为的认为它们属于一个模块,但是程序并不能区分哪些函数是同一个模块,如果仅仅从代码的角度来说,这没有任何模块的概念。
存在的问题:
- 污染了全局变量,无法保证不与其他模块发生变量名冲突。
- 模块成员之间看不出直接关系。
2. 对象封装-命名空间
var calculator = {
add: function(a, b) {
return parseFloat(a) + parseFloat(b);
},
subtract: function(a, b) {},
multiply: function(a, b) {},
divide: function(a, b) {}
};
通过添加命名空间的形式从某种程度上解决了变量命名冲突的问题,但是并不能从根本上解决命名冲突。 不过此时从代码级别可以明显区分出哪些函数属于同一个模块。
存在的问题:
- 暴露了所有的模块成员,内部状态可以被外部改写,不安全。
- 命名空间越来越长。
3. 私有公有成员分离
var calculator = (function () {
// 这里形成一个单独的私有的空间
// 私有成员的作用:
// 1、将一个成员私有化
// 2、抽象公共方法(其他成员中会用到的)
// 私有的转换逻辑
function convert(input){
return parseInt(input);
}
function add(a, b) {
return convert(a) + convert(b);
}
function subtract(a, b) {}
function multiply(a, b) {}
function divide(a, b) {}
return {
add : add,
subtract : subtract,
multiply : multiply,
divide : divide
}
})();
- 利用此种方式将函数包装成一个独立的作用域,私有空间的变量和函数不会影响到全局作用域。
- 以返回值的方式得到模块的公共成员,公开公有方法,隐藏私有空间内部的属性、元素,比如注册方法中可能会记录日志。
- 可以有选择的对外暴露自身成员。
- 从某种意义上来说,解决了变量命名冲突的问题。
4. 模块的扩展与维护
// 计算模块
(function (calculator) {
function convert(input) {
return parseInt(input);
}
calculator.add = function(a, b) {
return convert(a) + convert(b);
}
window.calculator = calculator;
})(window.calculator || {});
// 新增需求
(function (calculator) {
calculator.remain = function (a , b) {
return a % b;
}
window.calculator = calculator;
})(window.calculator || {});
alert(calculator.remain(4,3));
- 利用此种方式,有利于对庞大的模块的子模块划分。
- 实现了开闭原则:对新增开放,对修改关闭。对于已有文件尽量不要修改,通过添加新文件的方式添加新功能。
5. 第三方依赖的管理
(function (calculator , $) {
// 依赖函数的参数,是属于模块内部
// console.log($);
calculator.remain = function (a , b) {
return a % b;
}
window.calculator = calculator;
})(window.calculator || {} , jQuery);
模块最好要保证模块的职责单一性,最好不要与程序的其他部分直接交互,通过向匿名函数注入依赖项的形式,除了保证模块的独立性,还使模块之间的以来关系变得明显。
对于模块的依赖通过自执行函数的参数传入,这样做可以做到依赖抽象,本例中使用的jQuery,而当要使用zepto的时候,只要更换传入的参数即可。
原则:高内聚低耦合,模块内相关性高,模块间关联低。
总结:在什么场景下使用模块化开发
- 业务复杂
- 重用逻辑非常多
- 扩展性要求较高
三、模块化规范
服务器端规范主要是CommonJS,node.js用的就是CommonJS规范。
客户端规范主要有:AMD(异步模块定义,推崇依赖前置)、CMD(通用模块定义,推崇依赖就近)。AMD规范的实现主要有RequireJS,CMD规范的主要实现有SeaJS。RequireJS在国外用的比较多,SeaJS在国内用的比较多,并且SeaJS的创始人为阿里的玉伯,所以SeaJS在阿里系用的非常广泛,包括京东等大厂也在用SeaJS,我们详细介绍的也是SeaJS。但是SeaJS已经停止维护了,因为在ES6中已经有了模块化的实现,随着ES6的普及,第三方的模块化实现将会慢慢的淘汰(但是这个在国内可能还要很多年)。