全局變量是魔鬼,這句話在javascript存在的地方應該就是成立的,當然firefox擴展也不例外,如果大家把多于一個的對象置于全局命名空間下,和其他擴展的沖突是很容易發生的,而且發現這種沖突引起的錯誤是很困難的,因為每個人的擴展列表都不一樣啊。避免全局名字污染已經成了一個基本原則,本文從這點引申,介紹了一個應用在firebug中的擴展架構模式,非常值得推薦。
【原文】firefox extensions: global namespace pollution
【作者】jan odvarko
【譯文】http://cuimingda.com/2009/01/
【譯者】明達
以下是對原文的翻譯:
最近有幾個開發者向我咨詢如何設計firefox擴展的架構,第一個顯現在我腦海中的答案就是要合理定義那些在chromewindow作用域下的全局變量。
不合理的定義全局變量,可以輕易的引發不同擴展之間的沖突,而這些完全是應該避免的(這也是amo審閱的步驟之一),因為沖突所引發的問題是很難被發現的。就目前的開發環境來說,全局變量就是魔鬼,尤其是采用oop開發模式的時候。
我不想重復介紹如何從頭開始開發一個firefox擴展,對于這方面已經有很多非常詳細的文章。本文的重點放在如何設計一個更加易于維護的firefox擴展架構。
如果你對前面的介紹感興趣,那就接著看吧。。。
命名空間架構
擴展之間發生沖突的重要原因就是因為定義了不合理的全局變量。我認為對每個擴展來說,只有一個全局變量已經很足夠了(可以根據擴展的信息來定義這個唯一的全局變量的名字,比如可以是擴展的名字、域名、地址等),不僅可以滿足我們的開發,而且可以避免那些令人討厭的沖突。
firebug使用的命名空間架構,基本建立在著名的module pattern基礎上(這種模式最早由douglas crockfod定義)下的。這種模式簡單而清晰,但其實我在很長時間里都不是很明確這種模式究竟是如何工作的(i hadn’t understand how it actually works for a long time)。我相信每個開發者都可以充分利用這個方法。
基本的思路是將每個javascript腳本文件放進自己的作用域,這是通過一個函數來實現的,沒有定義任何全局變量,比如下面這段代碼:
function() {
// todo: 腳本文件中的全部代碼
}
我管這個函數就叫做命名空間。擺在眼前的第一個問題是,如何確定這個函數的內容會在正確的時間被調用。第二個問題是,如何在多個腳本文件中共享對象(這個會在后面的章節解答)。 firebug通過將所有的命名空間進行注冊,并在firefox chrome ui加載的時候調用來解決第一個問題,也就是下面這段代碼:
myextension.ns(function()
{
// todo: 腳本文件中的全部代碼
});
命名空間(就是原來定義的那個函數)為當作myextension.ns函數的一個參數,而myextension對象是這個擴展中定義的唯一全局變量。這個對象代表著整個擴展。不用擔心這個名字太長,我們可以為他建立個快捷方式(在實際開發中,這個名字可能會類似 comsoftwareishardmyextension這個樣子)。
ns函數比較簡單,就是把所有的方法都添加到一個數組中。
var namespaces = [];
this.ns = function(fn)
{
var ns = {};
namespaces.push(fn, ns);
return ns;
};
執行已注冊命名空間的函數,不可以命名為apply,別的什么名字都可以。
this.initialize = function() {
for (var i = 0; i < namespaces.length; i += 2) {
var fn = namespaces[i];
var ns = namespaces[i + 1];
fn.apply(ns);
}};
現在,然我們把前面的代碼連起來,看看全局擴展對象是如何定義和初始化的。
|||
下面這些代碼是browseroverlay.js文件的內容,這個腳本文件會在一個界面文件(browseroverlay.xul)中被引用。
// 擴展對應的唯一全局變量
var myextension = {};
(function() { // 注冊命名空間
var namespaces = [];
this.ns = function(fn) {
var ns = {};
namespaces.push(fn, ns);
return ns;
};
// 初始化
this.initialize = function() {
for (var i = 0; i < namespaces.length; i += 2) {
var fn = namespaces[i];
var ns = namespaces[i + 1];
fn.apply(ns);
}
};
// 收尾的清理工作
this.shutdown = function() {
window.removeeventlistener("load", myextension.initialize, false);
window.removeeventlistener("unload", myextension.shutdown, false);
};
// 注冊兩個事件處理程序,維護擴展的生存期
window.addeventlistener("load", myextension.initialize, false);
window.addeventlistener("unload", myextension.shutdown, false);
}).apply(myextension);
正如我前文所述,這里只有一個全局對象myextension。
總結一下,這個對象要實現下面幾個方法:
當然這段代碼也會確保initialize和shutdown方法會在正確的時間被調用,這也是兩個事件處理程序的作用。
browseroverlay.xul現在看起來可能會是下面這個樣子:
<?xml version="1.0"?>
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/
there.is.only.xul">
<script src="chrome://namespace/content/browseroverlay.js" type="application/x-javascript"/>
<script src="chrome://namespace/content/module1.js" type="application/x-javascript"/>
<script src="chrome://namespace/content/module2.js" type="application/x-javascript"/>
</overlay>
在這里,module1.js和module2.js兩個文件是一模一樣的。
myextension.ns(function() {
// todo: 腳本內的全部代碼
});
在不同的模塊間共享數據
我們已經把所有的腳本置于本地的作用域下,現在讓我們來回答上面提到的第二個問題,就是在不同的命名空間下如何共享函數和數據?;镜乃悸樊斎皇且梦覀兾ㄒ坏娜謱ο罄玻簿褪莔yextension。
首先,讓我們先來看看下面這段代碼(都在lib.js文件中)
myextension.lib = {
// 共享函數接口
getcurrenturi: function() {
return window.location.href;
},
// 擴展對象的快捷方式
theapp: myextension,
// xpcom組件的快捷方式
cc: components.classes,
ci: components.interfaces,
// 等等。。。
};
你可以注意到,這段代碼在全局的myextension對象下建立了一個新的lib屬性,這個屬性定義了一個函數庫,是要在擴展所有的模塊中共享的。你應該在java的包結構中看到過相同的做法,所有的命名空間呈樹狀結構分布在一個唯一的對象下面,yui也是這樣子做的。
lib.js文件也在browseroverlay.xul中引入,緊隨browseroverlay.js的后面。
<?xml version="1.0"?>
<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/
there.is.only.xul">
<script src="chrome://myextension/content/browseroverlay.js" type="application/x-javascript"/>
<script src="chrome://myextension/content/lib.js" type="application/x-javascript"/>
<script src="chrome://myextension/content/module1.js" type="application/x-javascript"/>
<script src="chrome://myextension/content/module2.js" type="application/x-javascript"/>
</overlay>
讓我們對模塊內的腳本也做一些改進。
myextension.ns(function() {
with(myextension.lib) {
// todo: 腳本內的全部代碼
var modulevariable = "accessible only from withing this module";
dump("myextension.module initialization " + getcurrenturi() + "/n");
}
});
通過利用with語句,我們可以方便的訪問所有的庫函數,就像訪問全局變量一樣。
既然我們要訪問全局對象,還可以像下面這樣利用theapp這個快捷方式(尤其是命名空間名字太長的時候)
myextension.ns(function() {
with(myextension.lib) {
// todo: 腳本內的全部代碼
theapp.sharedvalue = "a new shared property";
}
});
下面這個圖是從uml的角度來縱觀整個架構。
大家可以在 這里 下載本文提到的演示擴展。
新聞熱點
疑難解答