关于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 function代码
May 23 Javascript
网络之美 JavaScript中Get和Set访问器的实现代码
Sep 19 Javascript
jquery(live)中File input的change方法只起一次作用的解决办法
Oct 21 Javascript
Node调试工具JSHint的安装及配置教程
May 27 Javascript
ECMAScript中函数function类型
Jun 03 Javascript
Javascript编写俄罗斯方块思路及实例
Jul 07 Javascript
浅析jQuery Mobile的初始化事件
Dec 03 Javascript
jQuery.Form实现Ajax上传文件同时设置headers的方法
Jun 26 jQuery
React-Native之定时器Timer的实现代码
Oct 04 Javascript
详解jQuery设置内容和属性
Apr 11 jQuery
15 分钟掌握vue-next响应式原理
Oct 13 Javascript
详解 javascript对象创建模式
Oct 30 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
文章推荐系统(三)
2006/10/09 PHP
深入解析PHP的引用计数机制
2013/06/14 PHP
PHP判断用户是否已经登录(跳转到不同页面或者执行不同动作)
2016/09/22 PHP
php自定义函数实现汉字转换utf8编码的方法
2016/09/29 PHP
php正则提取html图片(img)src地址与任意属性的方法
2017/02/08 PHP
PHP封装的完整分页类示例
2018/08/21 PHP
laravel5.2表单验证,并显示错误信息的实例
2019/09/29 PHP
javascript 星级评分效果(手写)
2012/12/24 Javascript
jQuery aminate方法定位到页面具体位置
2013/12/26 Javascript
js实现文字垂直滚动和鼠标悬停效果
2015/12/31 Javascript
DOM事件探秘篇
2017/02/15 Javascript
详解vue 组件之间使用eventbus传值
2017/10/25 Javascript
jquery 获取索引值在一定范围的列表方法
2018/01/25 jQuery
JavaScript实现新年倒计时效果
2018/11/17 Javascript
详解Vue依赖收集引发的问题
2019/04/22 Javascript
vuex实现像调用模板方法一样调用Mutations方法
2019/11/06 Javascript
基于JavaScript实现控制下拉列表
2020/05/08 Javascript
浅谈vue单页面中有多个echarts图表时的公用代码写法
2020/07/19 Javascript
element中table高度自适应的实现
2020/10/21 Javascript
[50:11]2018DOTA2亚洲邀请赛 4.7总决赛 LGD vs Mineski 第三场
2018/04/09 DOTA
利用python如何处理nc数据详解
2018/05/23 Python
Django2.1集成xadmin管理后台所遇到的错误集锦(填坑)
2018/12/20 Python
python如何实现代码检查
2019/06/28 Python
Python实现银行账户资金交易管理系统
2020/01/03 Python
python tkinter之 复选、文本、下拉的实现
2020/03/04 Python
keras:model.compile损失函数的用法
2020/07/01 Python
ivx平台开发之不用代码实现一个九宫格抽奖功能
2021/01/27 HTML / CSS
英国领先的酒杯和水晶玻璃器皿制造商:Dartington Crystal
2019/06/23 全球购物
Ever New美国:澳大利亚领先的女装时尚品牌
2019/11/28 全球购物
动物科学专业毕业生的自我评价
2013/11/29 职场文书
春季运动会广播稿大全
2014/02/19 职场文书
副科级后备干部考察材料
2014/05/15 职场文书
2014大四本科生自我鉴定总结
2014/10/04 职场文书
场地使用证明模板
2014/10/25 职场文书
Python中可变和不可变对象的深入讲解
2021/08/02 Python
浅谈Redis变慢的原因及排查方法
2022/06/21 Redis