【关于模块化以及为什么要模块化】
先说说我们为什么要模块化吧。其实这还是和编码思想和代码管理的便利度相关(没有提及名字空间污染的问题是因为我相信已经考虑到模块化思想的编码者应该至少有了一套自己的命名法则,在中小型的站点中,名字空间污染的概率已经很小了,但也不代表不存在,后面会说这个问题)。
其实模块化思想还是和面向对象的思想如出一辙,只不过可能我们口中所谓的“模块”是比所谓的“对象”更大的对象而已。我们把致力完成同一个目的的功能函数通过良好的封装组合起来,并且保证其良好的复用性,我们大概可以把这样一个组合代码片段的思想称为面向对象的思想。这样做的好处有很多,比如:易用性,通用性,可维护性,可阅读性,规避变量名污染等等。
而模块化无非就是在面向对象上的面向模块而已,我们把和同一个项目(模块)相关的功能封装有机的组合起来,通过一个共同的名字来管理。就大概可以说是模块化的思想。所以,相比面向对象而言的话,我觉得在代码架构上贯彻模块化的思想其实比面向对象的贯彻还更为容易一些。
不像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下可以看到我们新注册的名字空间
可以看到新注册的Hongru命名空间已经生效:再看看Module的模块队列:
可以发现,新注册的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名字空间下
可以看到测试结果:
本来定义在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];
}
}