Board logo

标题: Javascript模块化和命名空间管理 [打印本页]

作者: qingqing3721    时间: 2011-6-22 10:12     标题: Javascript模块化和命名空间管理

【关于模块化以及为什么要模块化】

  先说说我们为什么要模块化吧。 其实这还是和编码思想和代码管理的便当度相关。
  其实模块化思想还是和面向对象的思想如出一辙, 只不过能够我们口中所谓的“模块”是比所谓的“对象”更大的对象而已。 我们把致力完成同一个目的的功用函数通过良好的封装组合起来, 并且保证其良好的复用性, 我们大概可以把这样一个组合代码片段的思想称为面向对象的思想。 这样做的好处有很多, 比如:易用性, 通用性, 可维护性, 可阅读性, 规避变量名污染等等。
  而模块化无非就是在面向对象上的面向模块而已, 我们把和同一个项目(模块)相关的功用封装无机的组合起来, 通过一个共同的名字来管理。 就大概可以说是模块化的思想。 所以, 相比面向对象而言的话, 我觉得在代码架构上贯彻模块化的思想其实比面向对象的贯彻还更为容易一些。
  不像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; iparts. length; i++) {
var part = parts;
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等别的属性, 等会会接着说。 下面是一个创立名字空间的测试demoModule. 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. length1  typeof arguments[1] == 'object'  arguments[1] != null) {
to = arguments[1];
firstsymbol = 2;
}

for (var a=firstsymbol; aarguments. length; a++) {
symbols. push(arguments[a]);
}

if (symbols. length == 0) {
//default export list
if (from. EXPORT) {
for (var i=0; ifrom. EXPORT. length; i++) {
var s = from. EXPORT;
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; iform. EXPORT. length; i++) {
allowed[from. EXPORT] = true;
}
}
if (from. EXPORT_OK) {
for (var i=0; iform. EXPORT_OK. length; i++) {
allowed[form. EXPORT_OK] = true;
}
}
}

}
//import the symbols
for (var i=0; isymbols. length; i++) {
var s = symbols;
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];
}
}

这个办法中第一个参数为导出源空间, 第二个参数为导入目的空间(可选???硎嵌ㄒ宓膅lobalNamespace), 前面的参数也是可选???阆氲汲龅木咛迨粜曰虬旆ǎ?默许是标记队列里的全部。
下面是测试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威望指南∽ㄐ模块和名字空间那节。文章由香袭人官网旗舰店整理,收集辛苦,希望能保留出处。




欢迎光临 编程开发论坛 (http://bbs.lihuasoft.net/) Powered by Discuz! 6.0.0