DIV CSS 佈局教程網

 DIV+CSS佈局教程網 >> 網頁腳本 >> JavaScript入門知識 >> 關於JavaScript >> 關於Javascript模塊化和命名空間管理的問題說明
關於Javascript模塊化和命名空間管理的問題說明
編輯:關於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; 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];
}
}
XML學習教程| jQuery入門知識| AJAX入門| Dreamweaver教程| Fireworks入門知識| SEO技巧| SEO優化集錦|
Copyright © DIV+CSS佈局教程網 All Rights Reserved