关于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
Oct 31 Javascript
鼠标选择动态改变网页背景颜色的JS代码
Dec 10 Javascript
node.js中的fs.createReadStream方法使用说明
Dec 17 Javascript
通过JS获取Request.QueryString()参数的值实现方法
Sep 27 Javascript
Bootstrap导航条学习使用(二)
Feb 08 Javascript
微信小程序实现的一键复制功能示例
Apr 24 Javascript
angular多语言配置详解
May 16 Javascript
redux.js详解及基本使用
May 24 Javascript
vue设置导航栏、侧边栏为公共页面的例子
Nov 01 Javascript
JavaScript实现像雪花一样的Hexaflake分形
Jul 07 Javascript
Vue实现背景更换颜色操作
Jul 17 Javascript
vue登录页实现使用cookie记住7天密码功能的方法
Feb 18 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
用cookies来跟踪识别用户
2006/10/09 PHP
让你同时上传 1000 个文件 (二)
2006/10/09 PHP
php获取网页里所有图片并存入数组的方法
2015/04/06 PHP
Aster vs Newbee BO5 第一场2.19
2021/03/10 DOTA
jquery $.ajax入门应用二
2008/11/19 Javascript
javascript string字符串优化问题
2011/07/31 Javascript
js获取IFRAME当前的URL的方法
2013/11/13 Javascript
原生js事件的添加和删除的封装
2014/07/01 Javascript
JS实现控制表格只显示行边框或者只显示列边框的方法
2015/03/31 Javascript
JavaScript模版引擎的基本实现方法浅析
2016/02/15 Javascript
基于jQuery Easyui实现登陆框界面
2017/07/10 jQuery
了解JavaScript函数中的默认参数
2019/05/30 Javascript
vue项目部署到nginx/tomcat服务器的实现
2019/08/26 Javascript
基于layui框架响应式布局的一些使用详解
2019/09/16 Javascript
将RGB值转换为灰度值的简单算法
2019/10/09 Javascript
微信小程序页面渲染实现方法
2019/11/06 Javascript
微信小程序顶部导航栏可滑动并选中放大
2019/12/05 Javascript
koa中间件核心(koa-compose)源码解读分析
2020/06/15 Javascript
在RedHat系Linux上部署Python的Celery框架的教程
2015/04/07 Python
python使用KNN算法手写体识别
2018/02/01 Python
Python多重继承的方法解析执行顺序实例分析
2018/05/26 Python
python针对不定分隔符切割提取字符串的方法
2018/10/26 Python
python-opencv获取二值图像轮廓及中心点坐标的代码
2019/08/27 Python
Python selenium实现断言3种方法解析
2020/09/08 Python
Python如何批量生成和调用变量
2020/11/21 Python
Python 内存管理机制全面分析
2021/01/16 Python
python爬虫破解字体加密案例详解
2021/03/02 Python
Electrolux伊莱克斯巴西商店:家用电器、小家电和配件
2018/05/23 全球购物
美国一站式电动和手动工具商店:International Tool
2020/11/26 全球购物
100%法国制造的游戏和玩具:Les Jouets Français
2021/03/02 全球购物
2014年人力资源工作总结
2014/11/19 职场文书
美丽的大脚观后感
2015/06/03 职场文书
在Django中使用MQTT的方法
2021/05/10 Python
Vue3中的Refs和Ref详情
2021/11/11 Vue.js
浅谈mysql哪些情况会导致索引失效
2021/11/20 MySQL
MySQL视图概念以及相关应用
2022/04/19 MySQL