本文講述如何爲mozilla添加一個新的協議。實現該協議將要使用javascript和XPCOM,也包括了將其添加到現有的mozilla程序中。
概述
Mozilla支持一般的網絡協議如HTTP和FTP。有時對現存的mozilla程序進行修改以使其支持一種新的協議非常有用,如此mozilla就可以更好的與其他應用程序集成。也許有的人想要mozilla在用戶點擊一個鏈接的時候運行一個IM客戶端,或者一個應用程序需要打開mozilla並在第一個頁面加載之前執行一個動作。要添加一個新的協議,需要實現一個XPCOM組件。由於XPCOM使得編程語言之間可以相互交互,因此,現在mozilla的XPCOM組件可以用C++或者javascript來實現。本文使用的是javascript,因爲它不需要編譯。
本文的例子是構建一個”search:”協議,它會使用用戶的默認搜索引擎來實現一次搜索。
XPCOM的shell
圖1(如下)展示了實現一個新的協議的XPCOM的javascript shell的一個基本結構。圖1:XPCOM shell的基本結構
// XPCOM constant definitions
// Protocol definition
// ProtocolFactory definition
// TestModule definition
function NSGetModule(){
return TestModule;
}
當XPCOM發現我們的組件的時候會調用NSGetModule(),其返回值是TestModule對象。TestModule實現了XPCOM調用的幾個方法,例如註冊新組件和獲取ProtocolFactory對象,ProtocolFactory運行mozilla在需要的時候創建一個Protocol對象。Protocol對象最終實現這個協議以及幾個XPCOM調用的方法。它包含了在mozilla需要解析該協議的時候運行的代碼,該方法叫做newChannel。
XPCOM shell的代碼快速的過一下,因爲在創建新的協議的時候它們都不需要修改。
XPCOM常量包括我們需要使用的一般的XPCOM組件,以及一些用於新組件的常量。kPROTOCOL_CID是該協議的一個唯一id號,每個新創建的協議都應該有一個使用uuid嘗試的唯一id。kSCHEME定義了該協議的調用方式——本例中是”x-search”,因此要從一個網站調用該協議需要使用"x-search:search term"。
圖2:使用的常量
const kSCHEME = "x-search";
const kPROTOCOL_NAME = "Search Protocol";
const kPROTOCOL_CONTRACTID = "@mozilla.org/network/protocol;1?name=" + kSCHEME;
const kPROTOCOL_CID = Components.ID("789409b9-2e3b-4682-a5d1-71ca80a76456");
// Mozilla defined
const kSIMPLEURI_CONTRACTID = "@mozilla.org/network/simple-uri;1";
const kIOSERVICE_CONTRACTID = "@mozilla.org/network/io-service;1";
const nsISupports = Components.interfaces.nsISupports;
const nsIIOService = Components.interfaces.nsIIOService;
const nsIProtocolHandler = Components.interfaces.nsIProtocolHandler;
const nsIURI = Components.interfaces.nsIURI;
下面是TestModule的代碼。registerSelf使用該組件定義的幾個常量註冊該組件,
圖3:TestModule的代碼
/**
* JS XPCOM component registration goop:
*
* We set ourselves up to observe the xpcom-startup category. This provides
* us with a starting point.
*/
var TestModule = new Object();
TestModule.registerSelf = function (compMgr, fileSpec, location, type)
{
compMgr = compMgr.QueryInterface(Components.interfaces.nsIComponentRegistrar);
compMgr.registerFactoryLocation(kPROTOCOL_CID,
kPROTOCOL_NAME,
kPROTOCOL_CONTRACTID,
fileSpec,
location,
type);
}
TestModule.getClassObject = function (compMgr, cid, iid)
{
if (!cid.equals(kPROTOCOL_CID))
throw Components.results.NS_ERROR_NO_INTERFACE;
if (!iid.equals(Components.interfaces.nsIFactory))
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
return ProtocolFactory;
}
TestModule.canUnload = function (compMgr)
{
return true;
}
ProtocolFactory所做的就是在實例化的時候都使用createInstance來做一些與XPCOM相關的錯誤檢驗,如果成功則返回一個Protocol對象。
圖4:ProtocolFactory代碼
var ProtocolFactory = new Object();
ProtocolFactory.createInstance = function (outer, iid)
{
if (outer != null)
throw Components.results.NS_ERROR_NO_AGGREGATION;
if (!iid.equals(nsIProtocolHandler) && !iid.equals(nsISupports))
throw Components.results.NS_ERROR_NO_INTERFACE;
return new Protocol();
}
最後,Protocol對象實現該協議的功能。newChannel()是在使用該協議的時候代碼運行在地點的方法。協議就是通道,爲了讓協議正常工作,其代碼需要實現一個通道。
圖5:Protocol的代碼
function Protocol()
{
}
Protocol.prototype =
{
QueryInterface: function(iid)
{
if (!iid.equals(nsIProtocolHandler) &&
!iid.equals(nsISupports))
throw Components.results.NS_ERROR_NO_INTERFACE;
return this;
},
scheme: kSCHEME,
defaultPort: -1,
protocolFlags: nsIProtocolHandler.URI_NORELATIVE |
nsIProtocolHandler.URI_NOAUTH,
allowPort: function(port, scheme)
{
return false;
},
newURI: function(spec, charset, baseURI)
{
var uri = Components.classes[kSIMPLEURI_CONTRACTID].createInstance(nsIURI);
uri.spec = spec;
return uri;
},
newChannel: function(input_uri)
{
// here goes the code that should be run when the protocol gets used.
}
},
}
協議的實現
最後一步是實現使用該協議的時候所調用的代碼。newChannel調用的時候需要帶有一個參數,是XPCOM的nsIUri類型。要獲取一個字符串版本可以使用其spec成員。該字符串包含了調用該協議所使用的完整URI,這裏是"x-search:[search terms]"。首先要做的是解析掉"x-search:"部分。dump() 是用來向控制檯打印信息的,在調試的時候很有用。圖6:參數處理
newChannel: function(aURI)
{
// aURI is a nsIUri, so get a string from it using .spec
var mySearchTerm = aURI.spec;
// strip away the kSCHEME: part
mySearchTerm = mySearchTerm.substring(mySearchTerm.indexOf(":") + 1, mySearchTerm.length);
mySearchTerm = encodeURI(mySearchTerm);
dump("[mySearchTerm=" + mySearchTerm + "]\n");
...
},
下一步就是獲取用戶的搜索引擎。下面的代碼來自mozilla的navigator.js文件。首先,默認的可靠搜索引擎的首選項是需要的。然後使用一個指向網絡搜索服務的數據RDF數據源來查詢用戶選擇的搜索引擎。
圖7:檢索搜索引擎
newChannel: function(input_uri)
{
...
var finalURL = "";
try{
// Get the preferences service
var prefService = Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefService);
var prefBranch = prefService.getBranch(null);
defaultSearchURL = prefBranch.getComplexValue("browser.search.defaulturl",
Components.interfaces.nsIPrefLocalizedString).data;
var searchDS = Components.classes["@mozilla.org/rdf/datasource;1?name=internetsearch"]
.getService(Components.interfaces.nsIInternetSearchService);
var searchEngineURI = prefBranch.getCharPref("browser.search.defaultengine");
if (searchEngineURI) {
searchURL = searchDS.GetInternetSearchURL(searchEngineURI, mySearchTerm, 0, 0, {value:0});
if (searchURL)
defaultSearchURL = searchURL;
}
dump("[Search Protocol Success: " + defaultSearchURL + "]")
} catch (e){
dump("[Search Protocol failed to get the search pref: " + e + "]\n");
}
finalURL = defaultSearchURL + mySearchTerm;
...
},
正如之前所描述的那樣,協議就是通道,所以newChannel必須返回一個通道。由於這個協議的目的是運行一次搜索,返回的通道有一些javascript來改變瀏覽器窗口的位置到搜索頁面。這可以通過獲取IOService服務,創建一個新的通道並從newChannel返回這個通道來實現。
圖8:創建一個通道
newChannel: function(input_uri)
{
...
/* create dummy nsIChannel instance */
var ios = Components.classes[kIOSERVICE_CONTRACTID]
.getService(nsIIOService);
return ios.newChannel("javascript:document.location.href='" + finalURL + "'", null, null);
},
最終newChannel的代碼如下圖9所示。完整的源代碼可以在這裏找到。
圖9:最終Protocol代碼
newChannel: function(input_uri)
{
// aURI is a nsIUri, so get a string from it using .spec
var mySearchTerm = aURI.spec;
// strip away the kSCHEME: part
mySearchTerm = mySearchTerm.substring(mySearchTerm.indexOf(":") + 1, mySearchTerm.length);
mySearchTerm = encodeURI(mySearchTerm);
dump("[mySearchTerm=" + mySearchTerm + "]\n");
var finalURL = "";
try{
// Get the preferences service
var prefService = Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefService);
var prefBranch = prefService.getBranch(null);
defaultSearchURL = prefBranch.getComplexValue("browser.search.defaulturl",
Components.interfaces.nsIPrefLocalizedString).data;
var searchDS = Components.classes["@mozilla.org/rdf/datasource;1?name=internetsearch"]
.getService(Components.interfaces.nsIInternetSearchService);
var searchEngineURI = prefBranch.getCharPref("browser.search.defaultengine");
if (searchEngineURI) {
searchURL = searchDS.GetInternetSearchURL(searchEngineURI, mySearchTerm, 0, 0, {value:0});
if (searchURL)
defaultSearchURL = searchURL;
}
dump("[Search Protocol Success: " + defaultSearchURL + "]")
} catch (e){
dump("[Search Protocol failed to get the search pref: " + e + "]\n");
}
finalURL = defaultSearchURL + mySearchTerm;
/* create dummy nsIURI and nsIChannel instances */
var ios = Components.classes[kIOSERVICE_CONTRACTID]
.getService(nsIIOService);
return ios.newChannel("javascript:document.location='" + finalURL + "'", null, null);
},
安裝協議
XPCOM組件在mozilla的mozillaDir/components目錄下。要安裝該搜索協議,複製完整的javascript文件到那個目錄。還需要告知Mozilla註冊這個組件(註冊了的組件列在components/目錄下的compreg.dat文件中),可以通過將一個叫做.autoreg的空文件放到mozilla的安裝目錄(與mozilla可執行程序同一級別)下來完成。在協議安裝完成之後,必須重新啓動mozilla來完成新組件的註冊。然後就可以通過輸入x-search:mozilla到url欄並按enter鍵來調用該協議。
譯註:
上述實例在gecko2.0之前的版本纔可用,經測試,將TestPprotocol.js文件放到firefox的components目錄下,然後在firefox的安裝目錄添加.autoreg文件之後重啓瀏覽器即可在components目錄下的compreg.dat文件中發現TestPprotocol.js已經成功註冊,在地址欄中輸入x-search:mozilla沒什麼反應(在the talk page for nsIProtocolHandler 提到到了上述例子代碼的一些不足,但沒有給出解決方案),但是在註冊該協議之前如果輸入這個並按回車會提示x-search協議未與任何應用程序關聯。