關於Javascript模組化和命名空間管理的問題說明

來源:互聯網
上載者:User

【關於模組化以及為什麼要模組化】

先說說我們為什麼要模組化吧。其實這還是和編碼思想和代碼管理的便利度相關(沒有提及名字空間汙染的問題是因為我相信已經考慮到模組化思想的編碼者應該至少有了一套自己的命名法則,在中小型的網站中,名字空間汙染的機率已經很小了,但也不代表不存在,後面會說這個問題)。
其實模組化思想還是和物件導向的思想如出一轍,只不過可能我們口中所謂的“模組”是比所謂的“對象”更大的對象而已。我們把致力完成同一個目的的功能函數通過良好的封裝組合起來,並且保證其良好的複用性,我們大概可以把這樣一個組合程式碼片段的思想稱為物件導向的思想。這樣做的好處有很多,比如:易用性,通用性,可維護性,可閱讀性,規避變數名汙染等等。
而模組化無非就是在物件導向上的面向模組而已,我們把和同一個項目(模組)相關的功能封裝有機的組合起來,通過一個共同的名字來管理。就大概可以說是模組化的思想。所以,相比物件導向而言的話,我覺得在代碼架構上貫徹模組化的思想其實比物件導向的貫徹還更為容易一些。
不像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];
}
}
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.