前端模块化开发
一、为什么要进行模块化开发
1.命名冲突
在实际工作中,相信大家都遇这样的问题:我自己测试好的代码和大家合并后怎么起冲突了?明明项目需要引入的包都引进来了怎么还报缺少包?……这些问题总结起来就是命名空间冲突及文件依赖加载顺序问题。举个最简单的例子来解释一下命名空间冲突问题,看下面这段代码:
test.html
<!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.js
/** * Created by user on 2016/5/14. */ var module=function(){ console.log("I am module2.js"); }
当运行test.html时结果输出:
显然是因为前两个JS文件里的函数名与html里面的一致而导致冲突,所以只会执行最后一个module()函数,在团队合作中你不会知道自己写的函数或变量等是否会与别人起冲突,为解决此类问题出现了参照于JAVA的命名空间如下:
test.html
<!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.js
/** * 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.js
/** * 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.……
以上很多问题都是因为文件依赖没有很好的管理起来。在前端页面里,大部分脚本的依赖目前依旧是通过人肉的方式保证。当团队比较小时,这不会有什么问题。当团队越来越大,公司业务越来越复杂后,依赖问题如果不解决,就会成为大问题。
二、什么是模块化开发
模块化开发使代码耦合度降低,模块化的意义在于最大化的设计重用,以最少的模块、零部件,更快速的满足更多的个性化需求。因为有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。但总不能随便写吧,总得有规范让大家遵守吧。
1.目前,模块化开发有:
1.服务器端规范:CommonJs---nodejs使用的规范,
2.浏览器端规范:AMD---RequireJS国外相对流行(官网)
2.SeaJS与RequireJS的对比:
a. 对于依赖的模块,AMD是提前执行,CMD是延后执行;
b. CMD推崇依赖就近,AMD推崇依赖前置;
c. AMD的API默认是一个当多个用,CMD的API严格区分,推崇职责单一。
三、怎么用模块化开发
直接看下面写的小型计算机器代码吧!
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的方法,从而解决了命名冲突问题。
四、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!'}; });
结果:
五、异步加载包
引入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'); })
六、使用第三方依赖库
比如当用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'); });
七、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等,在此不再赘述
作者:haoxl
出版:http://www.cnblogs.com/haoxl/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。