Reposted with permission from Jan Odvarko (AKA Honza):
I have been recently asked by couple of developers how to properly design architecture of a Firefox extension. The first thing that immediately came to my mind at that point was a problem with global variables defined by extensions in ChromeWindow scope.
This problem can easily cause collisions among various extensions. Something that should be always avoided (and is also part of AMO review process) since this kind of issues is very hard to find. Yes, global variables are still evil, especially in OOP world.
I don’t want to describe how to develop a new extension from scratch. For this there is already bunch of detailed articles. I am rather concentrating on effective tactics how to make Firefox extension architecture maintainable and well designed.
So, read more if you are interested…
Namespace Architecture
Defining global variables is a way how to risk collisions with other extensions. I think that creating just one global variable per extension that is unique enough (composed e.g. from the name of the extension, domain URL, etc.) is sufficient strategy how to avoid undesirable collisions.
The architecture for namespaces used in Firebug, is based (more or less) on well known module pattern (originally described by Douglas Crockford). It’s really simple and transparent so, I hadn’t understand how it actually works for a long time. I believe other extension developers can utilize this approach as well.
The basic idea is to wrap content of every JS file into its own scope that is represented by a function so, there are no global objects. See following snippet.
// TODO: entire JS code in this file is here
}
This is what I am going to call a namespace.
The first question is how to ensure that the function is actually called and the code executed at the right time. The second question is how to share objects among more files (see Sharing among namespaces chapter below). Firebug solves this by registering every namespace and executing all when Firefox chrome UI is ready. See modified example.
// TODO: entire JS code in this file is here
});
The namespace (regular function) is passed as a parameter to myExtension.ns function. The myExtension object is the only global object that is defined by the extension. This is the singleton object that represents entire extension. Don’t worry if the name is long, there’ll be a shortcut for it (in real application it could be e.g. comSoftwareIsHardMyExtension).
The ns function is simple. Every function is pushed into an array.
this.ns = function(fn) {
var ns = {};
namespaces.push(fn, ns);
return ns;
};
Actual execution of registered namespaces (functions) is only matter of calling apply on them.
for (var i=0; i<namespaces.length; i+=2) {
var fn = namespaces[i];
var ns = namespaces[i+1];
fn.apply(ns);
}
};
Now, let’s put all together and see how the global extension (singleton) object is defined and initialized.
The following source code snippet represents a browserOverlay.js file that is included into an overlay (browserOverlay.xul)
var myExtension = {};
(function() {
// Registration
var namespaces = [];
this.ns = function(fn) {
var ns = {};
namespaces.push(fn, ns);
return ns;
};
// Initialization
this.initialize = function() {
for (var i=0; i<namespaces.length; i+=2) {
var fn = namespaces[i];
var ns = namespaces[i+1];
fn.apply(ns);
}
};
// Clean up
this.shutdown = function() {
window.removeEventListener("load", myExtension.initialize, false);
window.removeEventListener("unload", myExtension.shutdown, false);
};
// Register handlers to maintain extension life cycle.
window.addEventListener("load", myExtension.initialize, false);
window.addEventListener("unload", myExtension.shutdown, false);
}).apply(myExtension);
As I mentioned above, there is just one global object myExtension.
To summarize, the object implements following methods:
- ns – register a new namespace.
- initialize – initialize all namespaces.
- shutdown – clean up.
And also, the code makes sure that initialize and shutdown methods are called at the right time. This is why event handlers are registered.
The browserOverlay.xul looks as follows now.
<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>
And the Module1.js and Module2.js files are both the same.
// TODO: entire JS code in this file is here
});
Sharing among namespaces
Now, when we have our script within a local scope(s), let’s answer the question how to share functionality and data among individual namespaces. The general idea is to use the one global object we have – myExtension.
First of all, see the following source code (lib.js file).
// Shared APIs
getCurrentURI: function() {
return window.location.href;
},
// Extension singleton shortcut
theApp: myExtension,
// XPCOM shortcuts
Cc: Components.classes,
Ci: Components.interfaces,
// Etc.
};
You can see that a new LIB property is created within our global myExtension singleton. This objects represents a library of functions that should be shared among all modules in our extension. At this point, you can also get inspiration from Java Packaging and create whole tree of namespaces within the global singleton (just like e.g. YUI does)
The lib.js file is included in browserOvelay.xul (just after browserOverlay.js)
<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>
Let’s also improve our module script a bit.
// TODO: entire JS code in this file is here
var moduleVariable = "Accessible only from withing this module";
dump("myExtension.Module initialization " + getCurrentURI() + "\n");
}});
By utilizing with statement we can simply access all library functions as if they would be a global functions.
In case we want to access our singleton global object we can also utilize theApp shortcut (useful especially if the name is long) as follows:
// TODO: entire JS code in this file is here
theApp.sharedValue = "A new shared property";
}});
Here is how the architecture look like from UML perspective.
Download example extension here.
Alex Vincent wrote on
Jorge Villalobos wrote on