关于Javascript模块化和命名空间管理的问题说明


Posted in Javascript onDecember 06, 2010

【关于模块化以及为什么要模块化】

先说说我们为什么要模块化吧。其实这还是和编码思想和代码管理的便利度相关(没有提及名字空间污染的问题是因为我相信已经考虑到模块化思想的编码者应该至少有了一套自己的命名法则,在中小型的站点中,名字空间污染的概率已经很小了,但也不代表不存在,后面会说这个问题)。
其实模块化思想还是和面向对象的思想如出一辙,只不过可能我们口中所谓的“模块”是比所谓的“对象”更大的对象而已。我们把致力完成同一个目的的功能函数通过良好的封装组合起来,并且保证其良好的复用性,我们大概可以把这样一个组合代码片段的思想称为面向对象的思想。这样做的好处有很多,比如:易用性,通用性,可维护性,可阅读性,规避变量名污染等等。
而模块化无非就是在面向对象上的面向模块而已,我们把和同一个项目(模块)相关的功能封装有机的组合起来,通过一个共同的名字来管理。就大概可以说是模块化的思想。所以,相比面向对象而言的话,我觉得在代码架构上贯彻模块化的思想其实比面向对象的贯彻还更为容易一些。
不像c#,java等这种本身就拥有良好模块化和命名空间机制的强类型语言。JavaScript并没有为创建和管理模块而提供任何语言功能。正因为这样,我们在做js的编码的某些时候,对于所谓的命名空间(namespace)的使用会显得有些过于随便(包括我自己)。比如 :

var Hongru = {} // namespace (function(){ 
Hongru.Class1 = function () { 
//TODO 
} 
... 
Hongru.Class2 = function () { 
//TODO 
} 
})();

如上,我们通常用一个全局变量或者全局对象就作为我们的namespace,如此简单,甚至显得有些随便的委以它这么重大的责任。但是我们能说这样做不好吗?不能,反而是觉得能有这种编码习惯的同学应该都值得表扬。。。

所以,我们在做一些项目的时候或者建一些规模不大的网站时,简单的用这种方式来做namespace的工作其实也够了,基本不会出什么大乱子。但是回归本质,如果是有代码洁癖或者是建立一个大规模的网站,抑或一开始就抱着绝对优雅的态度和逻辑来做代码架构的话。或许我们该考虑更好一些的namespace的注册和管理方式。
在这个方面,jQuery相比于YUI,Mootool,EXT等,就显得稍逊一筹,(虽然jq也有自己的一套模块化机制),但这依然不妨碍我们对它的喜爱,毕竟侧重点不同,jq强是强在它的选择器,否则他也不会叫j-Query了。
所以我们说jQuery比较适合中小型的网站也不无道理。就像豆瓣的开源的前端轻量级框架Do框架一样,也是建立在jQuery上,封装了一层模块化管理的思想和文件同步载入的功能。

【关于namespace】

好了,我们回归正题,如上的方式,简单的通过全局对象来做namespace已经能够很好的减少全局变量,规避变量名污染的问题,但是一旦网站规模变大,或者项目很多的时候,管理多个全局对象的名字空间依然会有问题。如果不巧发生了名字冲突,一个模块就会覆盖掉另一个模块的属性,导致其一或者两者都不能正常工作。而且出现问题之后,要去甄别也挺麻烦。所以我们可能需要一套机制或者工具,能在创建namespace的时候就能判断是否已有重名。

另一方面,不同模块,亦即不同namespace之间其实也不能完全独立,有时候我们也需要在不同名字空间下建立相同的方法或属性,这时方法或属性的导入和导出也会是个问题。

就以上两个方面,我稍微想了想,做了些测试,但依然有些纰漏。今天又重新翻了一下“犀牛书”,不愧是经典,上面的问题,它轻而易举就解决了。基于“犀牛书”的解决方案和demo,我稍微做了些修改和简化。把自己的理解大概分享出来。比较重要的有下面几个点:

--测试每一个子模块的可用性

由于我们的名字空间是一个对象,拥有对象应该有的层级关系,所以在检测名字空间的可用性时,需要基于这样的层级关系去判断和注册,这在注册一个子名字空间(sub-namespace)时尤为重要。比如我们新注册了一个名字空间为Hongru,然后我们需要再注册一个名字空间为Hongru.me,亦即我们的本意就是me这个namespace是Hongru的sub-namespace,他们应该拥有父子的关系。所以,在注册namespace的时候需要通过‘.'来split,并且进行逐一对应的判断。所以,注册一个名字空间的代码大概如下:

// create namespace --> return a top namespace 
Module.createNamespace = function (name, version) { 
if (!name) throw new Error('name required'); 
if (name.charAt(0) == '.' || name.charAt(name.length-1) == '.' || name.indexOf('..') != -1) throw new Error('illegal name'); var parts = name.split('.'); 
var container = Module.globalNamespace; 
for (var i=0; i<parts.length; i++) { 
var part = parts[i]; 
if (!container[part]) container[part] = {}; 
container = container[part]; 
} 
var namespace = container; 
if (namespace.NAME) throw new Error('module "'+name+'" is already defined'); 
namespace.NAME = name; 
if (version) namespace.VERSION = version; 
Module.modules[name] = namespace; 
return namespace; 
};

注:上面的Module是我们来注册和管理namespace的一个通用Module,它本身作为一个“基模块”,拥有一个modules的模块队列属性,用来存储我们新注册的名字空间,正因为有了这个队列,我们才能方便的判断namespace时候已经被注册:

var Module; 
//check Module --> make sure 'Module' is not existed 
if (!!Module && (typeof Module != 'object' || Module.NAME)) throw new Error("NameSpace 'Module' already Exists!"); Module = {}; 
Module.NAME = 'Module'; 
Module.VERSION = 0.1; 
Module.EXPORT = ['require', 
'importSymbols']; 
Module.EXPORT_OK = ['createNamespace', 
'isDefined', 
'modules', 
'globalNamespace']; 
Module.globalNamespace = this; 
Module.modules = {'Module': Module};

上面代码最后一行就是一个namespace队列,所有新建的namespace都会放到里面去。结合先前的一段代码,基本就能很好的管理我们的名字空间了,至于Module这个“基模块”还有些EXPORT等别的属性,等会会接着说。下面是一个创建名字空间的测试demo
Module.createNamespace('Hongru', 0.1);//注册一个名为Hongru的namespace,版本为0.1

上面第二个版本参数也可以不用,如果你不需要版本号的话。在chrome-debugger下可以看到我们新注册的名字空间

关于Javascript模块化和命名空间管理的问题说明
可以看到新注册的Hongru命名空间已经生效:再看看Module的模块队列:
关于Javascript模块化和命名空间管理的问题说明
可以发现,新注册的Hongru也添进了Module的modules队列里。大家也发现了,Module里还有require,isDefined,importSymbols几个方法。
由于require(检测版本),isDefined(检测namespace时候已经注册)这两个方法并不难,就稍微简略点:

--版本和重名检测 


// check name is defined or not 
Module.isDefined = function (name) { 
return name in Module.modules; 
}; 
// check version 
Module.require = function (name, version) { 
if (!(name in Module.modules)) throw new Error('Module '+name+' is not defined'); 
if (!version) return; 
var n = Module.modules[name]; 
if (!n.VERSION || n.VERSION < version) throw new Error('version '+version+' or greater is required'); 
};

上面两个方法都很简单,相信大家都明白,一个是队列检测是否重名,一个检测版本是否达到所需的版本。也没有什么特别的地方,就不细讲了,稍微复杂一点的是名字空间之间的属性或方法的相互导入的问题。
--名字空间中标记的属性或方法的导出
由于我们要的是一个通用的名字空间注册和管理的tool,所以在做标记导入或导出的时候需要考虑到可配置性,不能一股脑全部导入或导出。所以就有了我们看到的Module模板中的EXPORT和EXPORT_OK两个Array作为存贮我们允许导出的属性或方法的标记队列。其中EXPORT为public的标记队列,EXPORT_OK为我们可以自定义的标记队列,如果你觉得不要分这么清楚,也可以只用一个标记队列,用来存放你允许导出的标记属性或方法。
有了标记队列,我们做的导出操作就只针对EXPORT和EXPORT_OK两个标记队列中的标记。
// import module 
Module.importSymbols = function (from) { 
if (typeof form == 'string') from = Module.modules[from]; 
var to = Module.globalNamespace; //dafault 
var symbols = []; 
var firstsymbol = 1; 
if (arguments.length>1 && typeof arguments[1] == 'object' && arguments[1] != null) { 
to = arguments[1]; 
firstsymbol = 2; 
} 
for (var a=firstsymbol; a<arguments.length; a++) { 
symbols.push(arguments[a]); 
} 
if (symbols.length == 0) { 
//default export list 
if (from.EXPORT) { 
for (var i=0; i<from.EXPORT.length; i++) { 
var s = from.EXPORT[i]; 
to[s] = from[s]; 
} 
return; 
} else if (!from.EXPORT_OK) { 
// EXPORT array && EXPORT_OK array both undefined 
for (var s in from) { 
to[s] = from[s]; 
return; 
} 
} 
} 
if (symbols.length > 0) { 
var allowed; 
if (from.EXPORT || form.EXPORT_OK) { 
allowed = {}; 
if (from.EXPORT) { 
for (var i=0; i<form.EXPORT.length; i++) { 
allowed[from.EXPORT[i]] = true; 
} 
} 
if (from.EXPORT_OK) { 
for (var i=0; i<form.EXPORT_OK.length; i++) { 
allowed[form.EXPORT_OK[i]] = true; 
} 
} 
} 
} 
//import the symbols 
for (var i=0; i<symbols.length; i++) { 
var s = symbols[i]; 
if (!(s in from)) throw new Error('symbol '+s+' is not defined'); 
if (!!allowed && !(s in allowed)) throw new Error(s+' is not public, cannot be imported'); 
to[s] = form[s]; 
} 
}

这个方法中第一个参数为导出源空间,第二个参数为导入目的空间(可选,默认是定义的globalNamespace),后面的参数也是可选,为你想导出的具体属性或方法,默认是标记队列里的全部。
下面是测试demo:
Module.createNamespace('Hongru'); 
Module.createNamespace('me', 0.1); 
me.EXPORT = ['define'] 
me.define = function () { 
this.NAME = '__me'; 
} 
Module.importSymbols(me, Hongru);//把me名字空间下的标记导入到Hongru名字空间下

可以看到测试结果:
关于Javascript模块化和命名空间管理的问题说明 

 

 本来定义在me名字空间下的方法define()就被导入到Hongru名字空间下了。当然,这里说的导入导出,其实只是copy,在me名字空间下依然能访问和使用define()方法。

好了,大概就说到这儿吧,这个demo也只是提供一种管理名字空间的思路,肯定有更加完善的方法,可以参考下YUI,EXT等框架。或者参考《JavaScript权威指南》中模块和名字空间那节。

最后贴下源码:

/* == Module and NameSpace tool-func == 
* author : hongru.chen 
* date : 2010-12-05 
*/ 
var Module; 
//check Module --> make sure 'Module' is not existed 
if (!!Module && (typeof Module != 'object' || Module.NAME)) throw new Error("NameSpace 'Module' already Exists!"); 
Module = {}; 
Module.NAME = 'Module'; 
Module.VERSION = 0.1; 
Module.EXPORT = ['require', 
'importSymbols']; 
Module.EXPORT_OK = ['createNamespace', 
'isDefined', 
'modules', 
'globalNamespace']; 
Module.globalNamespace = this; 
Module.modules = {'Module': Module}; 
// create namespace --> return a top namespace 
Module.createNamespace = function (name, version) { 
if (!name) throw new Error('name required'); 
if (name.charAt(0) == '.' || name.charAt(name.length-1) == '.' || name.indexOf('..') != -1) throw new Error('illegal name'); 
var parts = name.split('.'); 
var container = Module.globalNamespace; 
for (var i=0; i<parts.length; i++) { 
var part = parts[i]; 
if (!container[part]) container[part] = {}; 
container = container[part]; 
} 
var namespace = container; 
if (namespace.NAME) throw new Error('module "'+name+'" is already defined'); 
namespace.NAME = name; 
if (version) namespace.VERSION = version; 
Module.modules[name] = namespace; 
return namespace; 
}; 
// check name is defined or not 
Module.isDefined = function (name) { 
return name in Module.modules; 
}; 
// check version 
Module.require = function (name, version) { 
if (!(name in Module.modules)) throw new Error('Module '+name+' is not defined'); 
if (!version) return; 
var n = Module.modules[name]; 
if (!n.VERSION || n.VERSION < version) throw new Error('version '+version+' or greater is required'); 
}; 
// import module 
Module.importSymbols = function (from) { 
if (typeof form == 'string') from = Module.modules[from]; 
var to = Module.globalNamespace; //dafault 
var symbols = []; 
var firstsymbol = 1; 
if (arguments.length>1 && typeof arguments[1] == 'object' && arguments[1] != null) { 
to = arguments[1]; 
firstsymbol = 2; 
} 
for (var a=firstsymbol; a<arguments.length; a++) { 
symbols.push(arguments[a]); 
} 
if (symbols.length == 0) { 
//default export list 
if (from.EXPORT) { 
for (var i=0; i<from.EXPORT.length; i++) { 
var s = from.EXPORT[i]; 
to[s] = from[s]; 
} 
return; 
} else if (!from.EXPORT_OK) { 
// EXPORT array && EXPORT_OK array both undefined 
for (var s in from) { 
to[s] = from[s]; 
return; 
} 
} 
} 
if (symbols.length > 0) { 
var allowed; 
if (from.EXPORT || form.EXPORT_OK) { 
allowed = {}; 
if (from.EXPORT) { 
for (var i=0; i<form.EXPORT.length; i++) { 
allowed[from.EXPORT[i]] = true; 
} 
} 
if (from.EXPORT_OK) { 
for (var i=0; i<form.EXPORT_OK.length; i++) { 
allowed[form.EXPORT_OK[i]] = true; 
} 
} 
} 
} 
//import the symbols 
for (var i=0; i<symbols.length; i++) { 
var s = symbols[i]; 
if (!(s in from)) throw new Error('symbol '+s+' is not defined'); 
if (!!allowed && !(s in allowed)) throw new Error(s+' is not public, cannot be imported'); 
to[s] = form[s]; 
} 
}
Javascript 相关文章推荐
Javascript 不能释放内存.
Sep 07 Javascript
jquery(live)中File input的change方法只起一次作用的解决办法
Oct 21 Javascript
js判读浏览器是否支持html5的canvas的代码
Nov 18 Javascript
实践中学习AngularJS表单
Mar 21 Javascript
bootstrap侧边栏圆点导航
Jan 11 Javascript
Vue.js学习教程之列表渲染详解
May 17 Javascript
关于express与koa的使用对比详解
Jan 25 Javascript
React注册倒计时功能的实现
Sep 06 Javascript
从Node.js事件触发器到Vue自定义事件的深入讲解
Jun 26 Javascript
微信小程序自定义扫码功能界面的实现代码
Jul 02 Javascript
在react项目中使用antd的form组件,动态设置input框的值
Oct 24 Javascript
详解vue 组件注册
Nov 20 Vue.js
javascript处理table表格的代码
Dec 06 #Javascript
菜鸟javascript基础资料整理3 正则
Dec 06 #Javascript
菜鸟javascript基础资料整理2
Dec 06 #Javascript
菜鸟javascript基础整理1
Dec 06 #Javascript
js 上传图片预览问题
Dec 06 #Javascript
兼容IE和FF的js脚本代码小结(比较常用)
Dec 06 #Javascript
DD_belatedPNG,IE6下PNG透明解决方案(国外)
Dec 06 #Javascript
You might like
php strcmp使用说明
2010/04/22 PHP
教大家制作简单的php日历
2015/11/17 PHP
24条货真价实的PHP代码优化技巧
2016/07/28 PHP
php结合ajax实现手机发红包的案例
2016/10/13 PHP
thinkPHP框架实现生成条形码的方法示例
2018/06/06 PHP
JavaScript类和继承 this属性使用说明
2010/09/03 Javascript
js常用代码段收集
2011/10/28 Javascript
js实现绿白相间竖向网页百叶窗动画切换效果
2015/03/02 Javascript
jQuery文字提示与图片提示效果实现方法
2016/07/04 Javascript
微信小程序 window_x64环境搭建
2016/09/30 Javascript
浅谈AngularJS中ng-class的使用方法
2016/11/11 Javascript
jQuery拖拽通过八个点改变div大小
2020/11/29 Javascript
JS实现的DIV块来回滚动效果示例
2017/02/07 Javascript
AngularJS中$http的交互问题
2017/03/29 Javascript
浅谈express 中间件机制及实现原理
2017/08/31 Javascript
详解使用Visual Studio Code对Node.js进行断点调试
2017/09/14 Javascript
浅谈React中的元素、组件、实例和节点
2018/02/27 Javascript
React styled-components设置组件属性的方法
2018/08/07 Javascript
JavaScript原型对象原理与应用分析
2018/12/27 Javascript
mpvue实现小程序签到金币掉落动画(api实现)
2019/10/17 Javascript
vue  elementUI 表单嵌套验证的实例代码
2019/11/06 Javascript
Python MySQLdb Linux下安装笔记
2015/05/09 Python
在Django的视图中使用form对象的方法
2015/07/18 Python
基于Python打造账号共享浏览器功能
2019/05/30 Python
Django基础知识 web框架的本质详解
2019/07/18 Python
django框架两个使用模板实例
2019/12/11 Python
Ratchet 模态框的实现
2020/08/19 HTML / CSS
人事部经理岗位职责
2014/03/07 职场文书
大专毕业生求职信
2014/07/05 职场文书
学生偷窃检讨书
2014/09/25 职场文书
领导干部作风建设工作总结
2014/10/23 职场文书
婚庆答谢词
2015/01/04 职场文书
学生检讨书
2015/01/27 职场文书
布达拉宫的导游词
2015/02/02 职场文书
企业财务人员岗位职责
2015/04/14 职场文书
小学生作文写作技巧100例,非常实用!
2019/07/08 职场文书