【转载】前端模块化开发
1、为何要进行模块化开发
1.命名冲突
在实际工做中,相信你们都遇这样的问题:我本身测试好的代码和你们合并后怎么起冲突了?明明项目须要引入的包都引进来了怎么还报缺乏包?……这些问题总结起来就是命名空间冲突及文件依赖加载顺序问题。举个最简单的例子来解释一下命名空间冲突问题,看下面这段代码:css
test.htmlhtml
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> <script src="js/module1.js"></script> <script src="js/module2.js"></script> </head> <body> </body> </html> <script> var module=function(){ console.log('I am module3'); }; module(); </script>
module1.js前端
/** * Created by user on 2016/5/14. */ var module=function(){ cosonle.log('I am module1.js'); }
module2.jsnode
/** * Created by user on 2016/5/14. */ var module=function(){ console.log("I am module2.js"); }
当运行test.html时结果输出:jquery
显然是由于前两个JS文件里的函数名与html里面的一致而致使冲突,因此只会执行最后一个module()函数,在团队合做中你不会知道本身写的函数或变量等是否会与别人起冲突,为解决此类问题出现了参照于JAVA的命名空间以下:git
test.htmlgithub
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> <script src="js/module1.js"></script> <script src="js/module2.js"></script> </head> <body> </body> </html> <script> var module=function(){ console.log('I am module3'); }; module1.fn.Utils.module(); module2.fn.Utils.module(); module(); </script>
module1.jsnpm
/** * Created by user on 2016/5/14. */ var module1={}; module1.fn={}; module1.fn.Utils={}; module1.fn.Utils.module=function(){ console.log("I am module1.js"); }
module2.jsapi
/** * Created by user on 2016/5/14. */ var module2={}; module2.fn={}; module2.fn.Utils={}; module2.fn.Utils.module=function(){ console.log("I am module2.js"); }
此时再运行test.html即可以输入全部的module里的值了浏览器
可是,写那么长的命名空间只为了调用一个方法,有没有感受有些啰嗦呢?此处我只是为了尽可能还原实际项目开发过程当中的问题而起了较长的命名空间名。将命名空间的概念在前端中发扬光大,首推 Yahoo! 的 YUI2 项目。下面是一段真实代码,来自 Yahoo! 的一个开源项目。
if (org.cometd.Utils.isString(response)) { return org.cometd.JSON.fromJSON(response); } if (org.cometd.Utils.isArray(response)) { return response; }
做为前端业界的标杆,YUI 团队下定决心解决这一问题。在 YUI3 项目中,引入了一种新的命名空间机制。
YUI().use('node', function (Y) { // Node 模块已加载好 // 下面能够经过 Y 来调用 var foo = Y.one('#foo'); });
YUI3 经过沙箱机制,很好的解决了命名空间过长的问题。然而,也带来了新问题。
YUI().use('a', 'b', function (Y) { Y.foo(); // foo 方法到底是模块 a 仍是 b 提供的? // 若是模块 a 和 b 都提供 foo 方法,如何避免冲突? });
暂且先不公布怎么解决此类问题,再看下一个问题。
2.文件依赖
开发最基本的原则就是不要重复,当项目中有多处地方运用同一个功能时,咱们就该想办法把它抽离出来作成util,当须要时直接调用它便可,可是若是你以后的代码依赖于util.js而你又忘了调用或者调用顺序出错,代码便报各类错误,举个最简单的例子,你们都知道Bootstrap依赖jquery,每次引入时都要将jquery放在Bootstrap前面,一两个相似于这样的依赖你或许还记得,但若是在庞大的项目中有许多这样的依赖关系,你还能清晰的记得吗?当项目愈来愈复杂,众多文件之间的依赖常常会让人抓狂。下面这些问题,我相信天天都在真实地发生着。
1.通用组更新了前端基础类库,却很难推进全站升级。
2.业务组想用某个新的通用组件,但发现没法简单经过几行代码搞定。
3.一个老产品要上新功能,最后评估只能基于老的类库继续开发。
4.公司整合业务,某两个产品线要合并。结果发现前端代码冲突。
5.……
以上不少问题都是由于文件依赖没有很好的管理起来。在前端页面里,大部分脚本的依赖目前依旧是经过人肉的方式保证。当团队比较小时,这不会有什么问题。当团队愈来愈大,公司业务愈来愈复杂后,依赖问题若是不解决,就会成为大问题。
2、什么是模块化开发
模块化开发使代码耦合度下降,模块化的意义在于最大化的设计重用,以最少的模块、零部件,更快速的知足更多的个性化需求。由于有了模块,咱们就能够更方便地使用别人的代码,想要什么功能,就加载什么模块。但总不能随便写吧,总得有规范让你们遵照吧。
1.目前,模块化开发有:
1.服务器端规范:CommonJs---nodejs使用的规范,
2.浏览器端规范:AMD---RequireJS国外相对流行(官网)
2.SeaJS与RequireJS的对比:
a. 对于依赖的模块,AMD是提早执行,CMD是延后执行;
b. CMD推崇依赖就近,AMD推崇依赖前置;
c. AMD的API默认是一个当多个用,CMD的API严格区分,推崇职责单一。
3、怎么用模块化开发
直接看下面写的小型计算机器代码吧!
test_seajs.html(前提是得去下载sea.js包哦,我是直接用命令npm install seajs下载的。)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Seajs体验</title> <script src="node_modules/seajs/dist/sea.js"></script> <script> // 在Seajs中模块的引入须要相对路径完整写法,注意不要再用script标签引入哦,不然用模块化就没意义了 seajs.use('./calculator.js', function(calculator) { //calculator其实就是calculator.js中的exports对象,这样即可以用其方法了 var ta = document.getElementById('txt_a'); var tb = document.getElementById('txt_b'); var tres = document.getElementById('txt_res'); var btn = document.getElementById('btn'); var op = document.getElementById('sel_op'); btn.onclick = function() { switch (op.value) { case '+': tres.value = calculator.add(ta.value, tb.value); break; case '-': tres.value = calculator.subtract(ta.value, tb.value); break; case 'x': tres.value = calculator.multiply(ta.value, tb.value); break; case '÷': tres.value = calculator.divide(ta.value, tb.value); break; } }; }); </script> </head> <body> <input type="text" id="txt_a"> <select id="sel_op"> <option value="+">+</option> <option value="-">-</option> <option value="x">x</option> <option value="÷">÷</option> </select> <input type="text" id="txt_b"> <input type="button" id="btn" value=" = "> <input type="text" id="txt_res"> </body> </html>
calculator.js文件内容以下:
/** * Created by user on 2016/5/14. */ // 定义一个模块,遵循Seajs的写法 define(function(require, exports, module) { // 此处是模块的私有空间 // 定义模块的私有成员 // 载入convertor.js模块 var convertor = require('./convertor.js'); function add(a, b) { return convertor.convertToNumber(a) + convertor.convertToNumber(b); } function subtract(a, b) { return convertor.convertToNumber(a) - convertor.convertToNumber(b); } function multiply(a, b) { return convertor.convertToNumber(a) * convertor.convertToNumber(b); } function divide(a, b) { return convertor.convertToNumber(a) / convertor.convertToNumber(b); } // 暴露模块的公共成员 exports.add = add; exports.subtract = subtract; exports.multiply = multiply; exports.divide = divide; });
convertor.js内容以下:
/** * 转换模块,导出成员:convertToNumber */ define(function(require, exports, module) { // 公开一些转换逻辑 exports.convertToNumber = function(input) { return parseFloat(input); } });
运行结果:
总结:在test_seajs.html用seajs.use引入calculator.js文件,而在calcultor.js文件中又require了convertor.js文件,这样就不用关心每一个js依赖关系啦,由于在其js内部就已经加载完成了。每引入一个js在其回调函数里执行其js的方法,从而解决了命名冲突问题。
4、seajs暴露接口
细心的同窗或许已经发现我在上面的calculator.js中用exports.xx暴露了该JS文件中的方法,若是里面有许多许多的方法,用exports都列出来多麻烦啊,其实还能够用module.exports来暴露其接口。以下:
test-exports.html
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> <script src="node_modules/seajs/dist/sea.js"></script> <script> // 1.当person.js用exports.Person=Person;暴露接口时须要如下方式进行使用其内部的方法 /*seajs.use('./person.js', function(e) { //此时的e为exports对象 var p=new e.Person(); p.sayHi(); });*/ //2.当person.js用module.exports暴露接口时须要如下方式进行使用其内部的方法 seajs.use('./person.js', function(Person) { //此时function里的参数便直接为Person对象 var p=new Person(); p.sayHi(); }); </script> </head> <body> </body> </html>
person.js
/** * Created by user on 2016/5/14. */ // 定义一个模块,遵循Seajs的写法 define(function(require, exports, module) { function Person(name, age, gender) { this.name = name; this.age = age; this.gender = gender; } Person.prototype.sayHi = function() { console.log('hi! I\'m a Coder, my name is ' + this.name); }; //exports.Person=Person; module.exports=Person; });
此时问题又来了,若是它俩同时存在以谁为准呢?答案是以module.exports为准,由于exports是module.exports的快捷方式,指向的仍然是原来的地址。看代码:
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title></title> <script src="node_modules/seajs/dist/sea.js"></script> <script> seajs.use('./person.js', function(e) { console.log(e); }); </script> </head> <body> </body> </html>
person.js
// 定义一个模块,遵循Seajs的写法 define(function(require, exports, module) { module.exports={name:'haoxiaoli'}; exports.name='hxl'; });
结果:
最后,其实还有一个return也能够暴露接口。它们的优先级为:return>module.exports>exports,看案例:
person.js
// 定义一个模块,遵循Seajs的写法 define(function(require, exports, module) { module.exports={name:'haoxiaoli'}; exports.name='hxl'; return {name:'hello world!'}; });
结果:
5、异步加载包
引入JS时不免会遇到须要异步加载文件的时候,此时require.async即可知足异步加载需求。以下demo
html文件
<script src="node_modules/seajs/dist/sea.js"></script> <script> // 在Seajs中模块的引入须要相对路径完整写法 seajs.use('./03-module1.js', function(e) { //console.log(e); }); </script>
03-module1.js文件
define(function(require,exports,module){ /*console.log('module1-------start'); //require必须执行完成后(./module2.js加载完成)才能够拿到返回值 var module2=require('./03-module2.js');//阻塞代码执行 //JS中阻塞如今会形成界面卡顿现象出现 console.log('module1--------end');*/ //异步加载便不会出现卡顿现象 console.log('module1--------start'); require.async('./03-module2.js',function(module2){ //等03-module2.js后再作的操做 });//此处不会阻塞代码执行 console.log('module1--------end'); })
6、使用第三方依赖库
好比当用CMD规范引入jquery时确定但愿它只在该模块内有效,而不是全局有效。在JQ中有对AMD规范的使用,但因为CMD属于国内的规范,人家并无对其进行适配,因此须要咱们手动去改造代码。在JQ中对AMD规范适配的下面增长以下代码。
if (typeof define === "function" && !define.amd) { // 当前有define函数,而且不是AMD的状况 // jquery在新版本中若是使用AMD或CMD方式,不会去往全局挂载jquery对象 define(function() { return jQuery.noConflict(true); }); }
这样再使用JQ时便作到此模块内可用了。
define(function(require,exports,module){ //用JQ作表明第三方库 var $=require('./jquery.js'); $(document.body).css('backgroundColor','red'); });
7、seajs配置
假如你项目中用到许多JS文件,或者引入的JS路径发生了变化,这样挨个去文件中修改有点不现实,因此能够把它们集中在某个页面进行统一管理文件路径,即config配置文件。如你在某个html文件中写:
<script src="node_modules/seajs/dist/sea.js"></script> <script> seajs.config({ alias:{ //给引入的包起别名,并放入到配置中 calc:'./05-calc.js' } }); seajs.use('calc'); </script>
每次引入的文件都在此配置后,路径改变了也只是在这一个文件中修改而已。另外还有map等,在此再也不赘述