关于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获得选中文本内容的方法
Dec 02 Javascript
为指定的元素添加遮罩层的示例代码
Jan 15 Javascript
jQuery调用RESTful WCF示例代码(GET方法/POST方法)
Jan 26 Javascript
JavaScript学习笔记之JS对象
Jan 22 Javascript
JavaScript实现表格点击排序的方法
May 11 Javascript
Javascript类型转换的规则实例解析
Feb 23 Javascript
AngularJS上拉加载问题解决方法
May 23 Javascript
JS+CSS3实现超炫的散列画廊特效
Jul 16 Javascript
解决option标签selected=&quot;selected&quot;属性失效的问题
Nov 06 Javascript
npm全局模块卸载及默认安装目录修改方法
May 15 Javascript
微信小程序实现提交input信息到后台的方法示例
Jan 19 Javascript
js实现课堂随机点名系统
Nov 21 Javascript
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中$_SERVER的详细参数与说明
2008/07/29 PHP
使用PHP实现微信摇一摇周边红包
2016/01/04 PHP
教你在header中隐藏php的版本信息
2016/08/10 PHP
php实现URL加密解密的方法
2016/11/17 PHP
Laravel事件监听器用法实例分析
2019/03/12 PHP
Yii框架函数简单用法分析
2019/09/09 PHP
JQuery EasyUI 对话框的使用方法
2010/10/24 Javascript
jQuery基础框架浅入剖析
2012/12/27 Javascript
判断js中各种数据的类型方法之typeof与0bject.prototype.toString讲解
2013/11/07 Javascript
javascript快速排序算法详解
2014/09/17 Javascript
jQuery实现预加载图片的方法
2015/03/17 Javascript
JavaScript动态提示输入框输入字数的方法
2015/07/27 Javascript
javascript的BOM
2016/05/03 Javascript
JavaScript中定义对象原型的两种使用方法
2016/12/15 Javascript
Web前端框架bootstrap实战【第一次接触使用】
2016/12/28 Javascript
微信小程序实战之运维小项目
2017/01/17 Javascript
JS组件系列之MVVM组件 vue 30分钟搞定前端增删改查
2017/04/28 Javascript
详解Vue中localstorage和sessionstorage的使用
2017/12/22 Javascript
Javascript实现时间倒计时功能
2018/11/17 Javascript
layer弹出层倒计时关闭的实现方法
2019/09/27 Javascript
vue计算属性+vue中class与style绑定(推荐)
2020/03/30 Javascript
python实现指定字符串补全空格的方法
2015/04/30 Python
pygame实现简易飞机大战
2018/09/11 Python
基于Numpy.convolve使用Python实现滑动平均滤波的思路详解
2019/05/16 Python
python利用JMeter测试Tornado的多线程
2020/01/12 Python
Pytorch GPU显存充足却显示out of memory的解决方式
2020/01/13 Python
tensorflow将图片保存为tfrecord和tfrecord的读取方式
2020/02/17 Python
CSS3的Border-radius轻松制作圆角
2012/12/24 HTML / CSS
教师自我鉴定范文
2013/11/10 职场文书
股权转让意向书
2014/04/01 职场文书
保卫工作个人总结
2015/03/03 职场文书
2015年实习班主任工作总结
2015/04/23 职场文书
退休职工欢送会致辞
2015/08/01 职场文书
虚拟机linux端mysql数据库无法远程访问的解决办法
2021/05/26 MySQL
如何使用PostgreSQL进行中文全文检索
2021/05/27 PostgreSQL
javascript进阶篇深拷贝实现的四种方式
2022/07/07 Javascript