javascript模块化基础
在接触javascript模块化后,慢慢发现了其中的原理,那么我已自己的观点,用演变的方式讲下javascript模块化。如果是大神请绕道,本文为初学小理解。
如果对JS基础了解较少的,请先看下作用域、原型对象、闭包、立即执行函数这些基础,之后在进行本文的学习。
- 最早在写一个带javascript的html时,会按照标准加入script标记,然后放入脚本代码。例如:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Roy.Li</title> </head> <body> <input type="text" id="txtPerson" name="txtPerson"> <input id="btnSearch" type="button" value="查询" /> <table id="tblPerson"></table> <script> // 源数据 var _personRepository = [ { id:1, name:'Gloria', age:18 }, { id:2, name:'Malcolm', age:19 }, { id:3, name:'Whitney', age:20 } ]; // 填充数据 function showPersonView(persons){ // 获取table元素 var tablePerson = document.getElementById('tblPerson'); tablePerson.innerHTML = ''; // 遍历persons并填充table var personCount = persons.length; for(var i = 0;i<personCount;i++){ var tr = document.createElement('tr'); for(var o in persons[i]){ if(typeof(persons[i][o]) != 'function'){ var td = document.createElement('td'); td.innerHTML = persons[i][o]; tr.appendChild(td); } } tablePerson.appendChild(tr); } } // 查询person function queryPersons(name){ var persons = []; if(name == ''){ persons = _personRepository; } for(var i = 0;i<_personRepository.length;i++){ if(_personRepository[i].name == name){ persons.push(_personRepository[i]); } } return persons } // 启动入口 window.onload = function () { document.getElementById('btnSearch').onclick = function(){ var personName = document.getElementById('txtPerson').value; var persons = queryPersons(personName); showPersonView(persons); }; } </script> </body> </html>
上面写了一个简单的html页面,查询展示人员信息的页面,可以看到在这个页面中script标签中一共有四部分内容,源数据构建、填充数据方法、查询数据方法、启动入口。在做完这个页面后,有些经验的开发者就会想,这样写虽然功能实现了,但是问题很多啊,我们就来看下都有哪些问题:
- 无法维护
- 全局作用域污染
- 容易产生命名冲突
- 管理依赖比较困难
- 那么这时聪明的开发者就开始做优化了,首先我们把内容按照功能进行分割,放到不同的JS文件中来加载。
我们首先在同级目录添加一个controller.js的文件,并将填充数据的方法剪切到此文件中。
// controller.js // 填充数据 function showPersonView(persons){ // 获取table元素 var tablePerson = document.getElementById('tblPerson'); tablePerson.innerHTML = ''; // 遍历persons并填充table var personCount = persons.length; for(var i = 0;i<personCount;i++){ var tr = document.createElement('tr'); for(var o in persons[i]){ if(typeof(persons[i][o]) != 'function'){ var td = document.createElement('td'); td.innerHTML = persons[i][o]; tr.appendChild(td); } } tablePerson.appendChild(tr); } }
接着在同级目录添加一个service.js文件,并且把源数据构建和数据查询方法剪切到此文件中。
// service.js // 源数据构建 var _personRepository = [ { id:1, name:'Gloria', age:18 }, { id:2, name:'Malcolm', age:19 }, { id:3, name:'Whitney', age:20 } ]; // 查询数据 function queryPersons(name){ var persons = []; if(name == ''){ persons = _personRepository; } for(var i = 0;i<_personRepository.length;i++){ if(_personRepository[i].name == name){ persons.push(_personRepository[i]); } } return persons }
最后在主页的index.html中引入这两个文件,且index.html的script代码只剩下启动入口了。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Roy.Li</title> <script type="text/javascript" src="controller.js"></script> <script type="text/javascript" src="service.js"></script> </head> <body> <input type="text" id="txtPerson" name="txtPerson"> <input id="btnSearch" type="button" value="查询" /> <table id="tblPerson"></table> <script> // 启动入口 window.onload = function () { document.getElementById('btnSearch').onclick = function(){ var personName = document.getElementById('txtPerson').value; var persons = queryPersons(personName); showPersonView(persons); }; } </script> </body> </html>
这次看上去是要好一点了,但是我们之前提到的问题并没有解决,只是在结构上看的比较清楚了而已。
- 有些开发者就开始用自己掌握的技术做文章了,我们使用对象来封装下吧,可以避免全局污染,这是就有了如下版本。
// controller.js var controller = new Object({ // 填充数据 showPersonView : function(tableElement,personName) { var persons = service.queryPersons(personName); tableElement.innerHTML = ''; // 遍历persons并填充table var personCount = persons.length; for(var i = 0;i<personCount;i++){ var tr = document.createElement('tr'); for(var o in persons[i]){ if(typeof(persons[i][o]) != 'function'){ var td = document.createElement('td'); td.innerHTML = persons[i][o]; tr.appendChild(td); } } tableElement.appendChild(tr); } } });
// service.js var service = new Object({ // 源数据构建 persons : [ { id:1, name:'Gloria', age:18 }, { id:2, name:'Malcolm', age:19 }, { id:3, name:'Whitney', age:20 } ], // 查询数据 queryPersons : function(personName) { var persons = []; if(personName == ''){ persons = this.persons; } for(var i = 0;i<this.persons.length;i++){ if(this.persons[i].name == personName){ persons.push(this.persons[i]); } } return persons } });
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Roy.Li</title> <script type="text/javascript" src="controller.js"></script> <script type="text/javascript" src="service.js"></script> </head> <body> <input type="text" id="txtPerson" name="txtPerson"> <input id="btnSearch" type="button" value="查询" /> <input type="button" value="恶意修改" onclick="javascript:service.persons = null" /> <table id="tblPerson"></table> <script> // 启动入口 window.onload = function () { document.getElementById('btnSearch').onclick = function(){ var personName = document.getElementById('txtPerson').value; var tableElement = document.getElementById('tblPerson'); controller.showPersonView(tableElement, personName); }; } </script> </body> </html>
我们运行起来可以看到,多了一个按钮,其他功能实现是一致的,我们首先查询下数据,然后点击下恶意修改按钮,再进行查询,发现查询功能不能使用啦,看来这原来的问题没搞定还出现新问题啦,呵呵。
那么我们对比下之前遇到的问题看下:
- 无法维护(搞定)
- 全局作用域污染(减轻)
- 容易产生命名冲突(搞定)
- 管理依赖比较困难
- 暴漏了所有成员不安全 (新问题)
- 看来对象封装的模块也是存在问题啊,这是又有开发者开始使用立即执行函数来封装我们的模块,于是又衍生了下面的版本:
// controller.js var controller = (function(){ // 填充数据 var showPersonView = function(tableElement,personName) { var persons = service.queryPersons(personName); tableElement.innerHTML = ''; // 遍历persons并填充table var personCount = persons.length; for(var i = 0;i<personCount;i++){ var tr = document.createElement('tr'); for(var o in persons[i]){ if(typeof(persons[i][o]) != 'function'){ var td = document.createElement('td'); td.innerHTML = persons[i][o]; tr.appendChild(td); } } tableElement.appendChild(tr); } }; return { showPersonView : showPersonView } })();
// service.js var service = (function(){ // 源数据构建 var persons = [ { id:1, name:'Gloria', age:18 }, { id:2, name:'Malcolm', age:19 }, { id:3, name:'Whitney', age:20 } ]; // 查询数据 var queryPersons = function(personName) { var _persons = []; if (personName == '') { _persons = persons; } for (var i = 0; i < persons.length; i++) { if (persons[i].name == personName) { _persons.push(persons[i]); } } return _persons }; return { queryPersons : queryPersons } })();
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Roy.Li</title> <script type="text/javascript" src="controller.js"></script> <script type="text/javascript" src="service.js"></script> </head> <body> <input type="text" id="txtPerson" name="txtPerson"> <input id="btnSearch" type="button" value="查询" /> <input type="button" value="恶意修改" onclick="javascript:service.persons = null" /> <table id="tblPerson"></table> <script> // 启动入口 window.onload = function () { document.getElementById('btnSearch').onclick = function(){ var personName = document.getElementById('txtPerson').value; var tableElement = document.getElementById('tblPerson'); controller.showPersonView(tableElement, personName); }; } </script> </body> </html>
这时我们运行起来就会发现,恶意修改无法修改我们的对象属性了,那么我们来看下立即执行函数所封装的模块是否满足我们的使用需求。
- 无法维护(搞定)
- 全局作用域污染(减轻)
- 容易产生命名冲突(搞定)
- 管理依赖比较困难
- 暴漏了所有成员不安全 (搞定)
可以看到,我们的管理依赖问题还是存在的,但是其他问题都解决掉了。
- 大神级开发人员又开始脑洞大开的想,依赖就是JS文件,我们既然不使用管理软件,那么是否可以自己手动封装一个动态加载的模块加载器呢,于是下面的版本诞生了:
首先我们需要在根目录下再新增一个文件,名为myRequire.js的一个文件,内容如下。
// myRequire.js // // 动态加载模块、模块导出、模块定义。 // var myRequire = (function(){ // 保存已经加载的模块。 // var _container = []; // 使用递归同步加载js文件。 // var _require = function(moduleNames){ for (var i = 0; i < moduleNames.length; i++){ var eleHead = document.getElementsByTagName('head')[0]; var eleScript = document.createElement('script'); eleScript.setAttribute('type', 'text/javascript'); eleScript.setAttribute('src', moduleNames.shift()); eleHead.appendChild(eleScript); eleScript.onload = function () { _require(moduleNames); }; } }; // 模块定义函数 // // moduleName: 模块名称 // depend: 模块依赖 // moduleFactory: 模块工厂 var _define = function(moduleName,depend,moduleFactory){ if(depend==''){ _container.push({name:moduleName,depend:'',module:moduleFactory()}); }else{ var dependModule; if(_container.length>0){ for(var i=0;i<_container.length;i++){ if(_container[i].name==depend) { dependModule = _container[i].module; break; }; } } if(dependModule){ var module = moduleFactory(dependModule); _container.push({name:moduleName,depend:depend,module:module}); } } }; // 模块导出函数 // // moduleName: 模块名称。 var _expose = function(moduleName){ var length = _container.length; for(var i=0;i<length;i++){ if(moduleName == _container[i].name){ return _container[i].module; } } }; return { require:_require, define:_define, expose:_expose }; })();
然后修改我们其他两个模块的构建方式,分别是controllerModule与serviceModule
// controller.js myRequire.define('controller.js', 'service.js', function(serviceModule){ // 填充数据 var showPersonView = function(tableElement,personName) { var persons = serviceModule.queryPersons(personName); tableElement.innerHTML = ''; // 遍历persons并填充table var personCount = persons.length; for(var i = 0;i<personCount;i++){ var tr = document.createElement('tr'); for(var o in persons[i]){ if(typeof(persons[i][o]) != 'function'){ var td = document.createElement('td'); td.innerHTML = persons[i][o]; tr.appendChild(td); } } tableElement.appendChild(tr); } }; return { showPersonView : showPersonView } })();
// service.js myRequire.define('service.js', '', function(){ // 源数据构建 var persons = [ { id:1, name:'Gloria', age:18 }, { id:2, name:'Malcolm', age:19 }, { id:3, name:'Whitney', age:20 } ]; // 查询数据 var queryPersons = function(personName) { var _persons = []; if (personName == '') { _persons = persons; } for (var i = 0; i < persons.length; i++) { if (persons[i].name == personName) { _persons.push(persons[i]); } } return _persons }; return { queryPersons : queryPersons } })();
修改主页中的js引用,只需引用myRequire.js,以及修改调用模块方式。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Roy.Li</title> <script src="myRequire.js"></script> </head> <body> <input type="text" id="txtPerson" name="txtPerson"> <input id="btnSearch" type="button" value="查询" /> <table id="tblPerson"></table> <script> // 启动入口 window.onload = function () { // 此处为同步加载,如果使用异步加载维护依赖就会比较困难,有兴趣的可以研究下。 myRequire.require(['service.js','controller.js']); document.getElementById('btnSearch').onclick = function(){ var personName = document.getElementById('txtPerson').value; var tableElement = document.getElementById('tblPerson'); // 获取控制器模块。 var controllerModule = myRequire.expose('controller.js'); controllerModule.showPersonView(tableElement, personName); }; } </script> </body> </html>
现在我们可以看到,我们所遇到的问题基本上都解决了,而且有一个比较合适的方式去封装我们自己的模块了,而且也封装了一个我们自己使用的模块加载器。
如果你现在已经对模块化有一个概念和了解了,那么你可以去尝试研究下目前主流已经有的模块化规范了,例如CommonJS、AMD、CMD等等,以及他们的最佳实现库,例如nodeJS、requireJS(有点像我们封装的myRequire)、seaJS等等,看下现在已有的javascript模块已经发展到什么程度了吧,PS:ECMA6发布后也支持class与module的写法了,只是现在还没有普及。
最后特别感谢gsafety - 郑硕提供的资料支持^_^。