关于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 相关文章推荐
jquery通过a标签删除table中的一行的代码
Dec 02 Javascript
调用HttpHanlder的几种返回方式小结
Dec 20 Javascript
jQuery使用addClass()方法给元素添加多个class样式
Mar 26 Javascript
详解JavaScript对Date对象的操作问题(生成一个倒数7天的数组)
Oct 01 Javascript
实现JavaScript高性能的数据存储
Dec 11 Javascript
javascript实现用户点击数量统计
Dec 25 Javascript
JavaScript实现三级级联特效
Nov 05 Javascript
Vue 去除路径中的#号
Apr 19 Javascript
jquery分页插件pagination使用教程
Oct 23 jQuery
vue中的双向数据绑定原理与常见操作技巧详解
Mar 16 Javascript
es6数组includes()用法实例分析
Apr 18 Javascript
Element Badge标记的使用方法
Jul 27 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调用Webservice实例代码
2011/07/29 PHP
PHP新手用的Insert和Update语句构造类
2012/03/31 PHP
php调用MySQL存储过程的方法集合(推荐)
2013/07/03 PHP
浅析php fwrite写入txt文件的时候用 \r\n不能换行的问题
2013/08/06 PHP
详解php中空字符串和0之间的关系
2016/10/23 PHP
PHP开启目录引索+fancyindex漂亮目录浏览带搜索功能
2019/09/23 PHP
学习YUI.Ext 第六天--关于树TreePanel(Part 1)
2007/03/10 Javascript
jQuery 浮动广告实现代码
2008/12/25 Javascript
jquery 关键字“拖曳搜索”之“拖曳”以及 图片“提示自适应放大”效果 的实现
2010/04/18 Javascript
jquery全选/全不选/反选另一种实现方法(配合原生js)
2013/04/07 Javascript
JavaScript获取图片像素颜色并转换为box-shadow显示
2016/03/11 Javascript
利用jquery制作滚动到指定位置触发动画
2016/03/26 Javascript
详解JavaScript中基于原型prototype的继承特性
2016/05/05 Javascript
JS简单判断函数是否存在的方法
2017/02/13 Javascript
详谈Node.js之操作文件系统
2017/08/29 Javascript
详解redis在nodejs中的应用
2018/05/02 NodeJs
vue组件是如何解析及渲染的?
2021/01/13 Vue.js
用Python读取几十万行文本数据
2018/12/24 Python
Python3.5实现的三级菜单功能示例
2019/03/25 Python
Python学习笔记之错误和异常及访问错误消息详解
2019/08/08 Python
python的pip有什么用
2020/06/17 Python
css3.0 图形构成实例练习二
2013/03/19 HTML / CSS
潘多拉珠宝英国官方网上商店:PANDORA英国
2018/06/12 全球购物
体育专业个人的求职信范文
2013/09/21 职场文书
医学生实习自我鉴定
2013/09/27 职场文书
童装店创业计划书
2014/01/09 职场文书
户外宣传策划方案
2014/05/25 职场文书
党性心得体会
2014/09/03 职场文书
爱护公共设施演讲稿
2014/09/13 职场文书
2014年庆祝国庆65周年演讲稿
2014/09/21 职场文书
广告设计专业毕业生自我鉴定
2014/09/27 职场文书
《画家和牧童》教学反思
2016/02/17 职场文书
高中化学教学反思
2016/02/22 职场文书
一篇文章带你深入了解Mysql触发器
2021/08/02 MySQL
html粘性页脚的具体使用
2022/01/18 HTML / CSS
Promise静态四兄弟实现示例详解
2022/07/07 Javascript