Revision 558813 of Creating Reusable Modules

  • Revision slug: Mozilla/Add-ons/SDK/Tutorials/Creating_reusable_modules
  • Revision title: Creating Reusable Modules
  • Revision id: 558813
  • Created:
  • Creator: wbamberg
  • Is current revision? No
  • Comment
Tags: 

Revision Content

To follow this tutorial you'll need to have installed the SDK and learned the basics of cfx.

With the SDK you don't have to keep all your add-on in a single "main.js" file. You can split your code into separate modules with clearly defined interfaces between them. You then import and use these modules from other parts of your add-on using the require() statement, in exactly that same way that you import core SDK modules like page-mod or panel.

It can often make sense to structure a larger or more complex add-on as a collection of modules. This makes the design of the add-on easier to understand and provides some encapsulation as each module will export only what it chooses to, so you can change the internals of the module without breaking its users.

Once you've done this, you can package the modules and distribute them independently of your add-on, making them available to other add-on developers and effectively extending the SDK itself.

In this tutorial we'll do exactly that with a module that calculate file hashes.

A hashing add-on

A hash function takes a string of bytes of any length, and produces a short, fixed length string of bytes as output. It's a useful way to create a "fingerprint" that can be used to identify a file. MD5 is a commonly used hash function: although it's no longer considered secure, it works fine outside a security context.

Here we'll write an add-on that lets the user select a file on disk and calculates its hash. For both these operations we'll use XPCOM interfaces.

File picker

To let the user select a file we'll use nsIFilePicker. The documentation for that interface includes an example which we can adapt like this:

var {Cc, Ci} = require("chrome");

function promptForFile() {
  const nsIFilePicker = Ci.nsIFilePicker;

  var fp = Cc["@mozilla.org/filepicker;1"]
           .createInstance(nsIFilePicker);

  var window = require("sdk/window/utils").getMostRecentBrowserWindow();
  fp.init(window, "Select a file", nsIFilePicker.modeOpen);
  fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText);

  var rv = fp.show();
  if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace) {
    var file = fp.file;
    // Get the path as string. Note that you usually won't
    // need to work with the string paths.
    var path = fp.file.path;
    // work with returned nsILocalFile...
  }
  return path;
}

Hash function

Firefox has built-in support for hash functions, exposed via the nsICryptoHash XPCOM interface The documentation page for that interface includes an example of calculating an MD5 hash of a file's contents, given its path. We can adapt it like this:

var {Cc, Ci} = require("chrome");

// return the two-digit hexadecimal code for a byte
function toHexString(charCode) {
  return ("0" + charCode.toString(16)).slice(-2);
}

function md5File(path) {
  var f = Cc["@mozilla.org/file/local;1"]
                  .createInstance(Ci.nsILocalFile);
  f.initWithPath(path);
  var istream = Cc["@mozilla.org/network/file-input-stream;1"]           
                        .createInstance(Ci.nsIFileInputStream);
  // open for reading
  istream.init(f, 0x01, 0444, 0);
  var ch = Cc["@mozilla.org/security/hash;1"]
                   .createInstance(Ci.nsICryptoHash);
  // we want to use the MD5 algorithm
  ch.init(ch.MD5);
  // this tells updateFromStream to read the entire file
  const PR_UINT32_MAX = 0xffffffff;
  ch.updateFromStream(istream, PR_UINT32_MAX);
  // pass false here to get binary data back
  var hash = ch.finish(false);

  // convert the binary hash data to a hex string.
  var s = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
  return s;
}

Putting it together

The complete add-on adds a button to Firefox: when the user clicks the button, we ask them to select a file, compute the hash, and log the hash to the console:

var {Cc, Ci} = require("chrome");

// return the two-digit hexadecimal code for a byte
function toHexString(charCode) {
  return ("0" + charCode.toString(16)).slice(-2);
}

function md5File(path) {
  var f = Cc["@mozilla.org/file/local;1"]
                  .createInstance(Ci.nsILocalFile);
  f.initWithPath(path);
  var istream = Cc["@mozilla.org/network/file-input-stream;1"]           
                        .createInstance(Ci.nsIFileInputStream);
  // open for reading
  istream.init(f, 0x01, 0444, 0);
  var ch = Cc["@mozilla.org/security/hash;1"]
                   .createInstance(Ci.nsICryptoHash);
  // we want to use the MD5 algorithm
  ch.init(ch.MD5);
  // this tells updateFromStream to read the entire file
  const PR_UINT32_MAX = 0xffffffff;
  ch.updateFromStream(istream, PR_UINT32_MAX);
  // pass false here to get binary data back
  var hash = ch.finish(false);

  // convert the binary hash data to a hex string.
  var s = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
  return s;
}

function promptForFile() {
  var window = require("sdk/window/utils").getMostRecentBrowserWindow();
  const nsIFilePicker = Ci.nsIFilePicker;

  var fp = Cc["@mozilla.org/filepicker;1"]
           .createInstance(nsIFilePicker);
  fp.init(window, "Select a file", nsIFilePicker.modeOpen);
  fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText);

  var rv = fp.show();
  if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace) {
    var file = fp.file;
    // Get the path as string. Note that you usually won't
    // need to work with the string paths.
    var path = fp.file.path;
    // work with returned nsILocalFile...
  }
  return path;
}

require("sdk/ui/button/action").ActionButton({
  id: "show-panel",
  label: "Show Panel",
  icon: {
    "16": "./icon-16.png"
  },
  onClick: function() {
    console.log(md5File(promptForFile()));
  }
});

This works , but main.js is now getting longer and its logic is harder to understand. Let's factor the file picker and hashing code into separate modules.

Creating separate modules

filepicker.js

First create a new file in "lib" called "filepicker.js", and copy the file picker code into this new file, and add the following line at the end:

Next, add the following line somewhere in the new file:

exports.promptForFile = promptForFile;

This defines the public interface of the new module.

So "filepicker.js" should look like this:

var {Cc, Ci} = require("chrome");

function promptForFile() {
  var window = require("sdk/window/utils").getMostRecentBrowserWindow();
  const nsIFilePicker = Ci.nsIFilePicker;

  var fp = Cc["@mozilla.org/filepicker;1"]
           .createInstance(nsIFilePicker);
  fp.init(window, "Select a file", nsIFilePicker.modeOpen);
  fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText);

  var rv = fp.show();
  if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace) {
    var file = fp.file;
    // Get the path as string. Note that you usually won't
    // need to work with the string paths.
    var path = fp.file.path;
    // work with returned nsILocalFile...
  }
  return path;
}

exports.promptForFile = promptForFile;

md5.js

Next, create another file in "lib", called "md5.js". Copy the hashing code there, and add this line at the end:

exports.hashFile = md5File;

The complete file looks like this:

var {Cc, Ci} = require("chrome");
var filepicker = require("./filepicker.js");

// return the two-digit hexadecimal code for a byte
function toHexString(charCode) {
  return ("0" + charCode.toString(16)).slice(-2);
}

function md5File(path) {
  var f = Cc["@mozilla.org/file/local;1"]
                  .createInstance(Ci.nsILocalFile);
  f.initWithPath(path);
  var istream = Cc["@mozilla.org/network/file-input-stream;1"]           
                        .createInstance(Ci.nsIFileInputStream);
  // open for reading
  istream.init(f, 0x01, 0444, 0);
  var ch = Cc["@mozilla.org/security/hash;1"]
                   .createInstance(Ci.nsICryptoHash);
  // we want to use the MD5 algorithm
  ch.init(ch.MD5);
  // this tells updateFromStream to read the entire file
  const PR_UINT32_MAX = 0xffffffff;
  ch.updateFromStream(istream, PR_UINT32_MAX);
  // pass false here to get binary data back
  var hash = ch.finish(false);

  // convert the binary hash data to a hex string.
  var s = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
  return s;
}

exports.hashFile = md5File;

main.js

Finally, update main.js to import these two new modules and use them:

var filepicker = require("./filepicker.js");
var md5 = require("./md5.js");

require("sdk/ui/button/action").ActionButton({
  id: "show-panel",
  label: "Show Panel",
  icon: {
    "16": "./icon-16.png"
  },
  onClick: function() {
    console.log(md5.hashFile(filepicker.promptForFile()));
  }
});

You can distribute these modules to other developers, too. They can copy them somewhere under the add-on, and include them using require() in the same way.

Learning More

To see some of the modules people have already developed, see the page of community-developed modules. To learn how to use third-party modules in your own code, see the tutorial on adding menu items.

Revision Source

<div class="note">
 To follow this tutorial you'll need to have <a href="/en-US/Add-ons/SDK/Tutorials/Installation">installed the SDK</a> and learned the <a href="/en-US/Add-ons/SDK/Tutorials/Getting_Started_With_cfx">basics of <code>cfx</code></a>.</div>
<p>With the SDK you don't have to keep all your add-on in a single "main.js" file. You can split your code into separate modules with clearly defined interfaces between them. You then import and use these modules from other parts of your add-on using the <code>require()</code> statement, in exactly that same way that you import core SDK modules like <a href="/en-US/Add-ons/SDK/High-Level_APIs/page_mod"><code>page-mod</code></a> or <a href="/en-US/Add-ons/SDK/High-Level_APIs/panel"><code>panel</code></a>.</p>
<p>It can often make sense to structure a larger or more complex add-on as a collection of modules. This makes the design of the add-on easier to understand and provides some encapsulation as each module will export only what it chooses to, so you can change the internals of the module without breaking its users.</p>
<p>Once you've done this, you can package the modules and distribute them independently of your add-on, making them available to other add-on developers and effectively extending the SDK itself.</p>
<p>In this tutorial we'll do exactly that with a module that calculate file hashes.</p>
<h2 id="A_hasing_add-on">A hashing add-on</h2>
<p>A hash function takes a string of bytes of any length, and produces a short, fixed length string of bytes as output. It's a useful way to create a "fingerprint" that can be used to identify a file. MD5 is a commonly used hash function: although it's no longer considered secure, it works fine outside a security context.</p>
<p>Here we'll write an add-on that lets the user select a file on disk and calculates its hash. For both these operations we'll use <a href="/en-US/docs/Mozilla/Tech/XPCOM">XPCOM</a> interfaces.</p>
<h3 id="File_picker">File picker</h3>
<p>To let the user select a file we'll use nsIFilePicker. The <a href="/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIFilePicker">documentation for that interface</a> includes an example which we can adapt like this:</p>
<pre class="brush: js">
var {Cc, Ci} = require("chrome");

function promptForFile() {
  const nsIFilePicker = Ci.nsIFilePicker;

  var fp = Cc["@mozilla.org/filepicker;1"]
           .createInstance(nsIFilePicker);

  var window = require("sdk/window/utils").getMostRecentBrowserWindow();
  fp.init(window, "Select a file", nsIFilePicker.modeOpen);
  fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText);

  var rv = fp.show();
  if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace) {
    var file = fp.file;
    // Get the path as string. Note that you usually won't
    // need to work with the string paths.
    var path = fp.file.path;
    // work with returned nsILocalFile...
  }
  return path;
}</pre>
<h3 id="Hash_function">Hash function</h3>
<p>Firefox has built-in support for hash functions, exposed via the nsICryptoHash XPCOM interface The <a href="/en-US/docs/XPCOM_Interface_Reference/nsICryptoHash">documentation page for that interface</a> includes an example of calculating an MD5 hash of a file's contents, given its path. We can adapt it like this:</p>
<pre class="brush: js">
var {Cc, Ci} = require("chrome");

// return the two-digit hexadecimal code for a byte
function toHexString(charCode) {
  return ("0" + charCode.toString(16)).slice(-2);
}

function md5File(path) {
  var f = Cc["@mozilla.org/file/local;1"]
                  .createInstance(Ci.nsILocalFile);
  f.initWithPath(path);
  var istream = Cc["@mozilla.org/network/file-input-stream;1"]           
                        .createInstance(Ci.nsIFileInputStream);
  // open for reading
  istream.init(f, 0x01, 0444, 0);
  var ch = Cc["@mozilla.org/security/hash;1"]
                   .createInstance(Ci.nsICryptoHash);
  // we want to use the MD5 algorithm
  ch.init(ch.MD5);
  // this tells updateFromStream to read the entire file
  const PR_UINT32_MAX = 0xffffffff;
  ch.updateFromStream(istream, PR_UINT32_MAX);
  // pass false here to get binary data back
  var hash = ch.finish(false);

  // convert the binary hash data to a hex string.
  var s = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
  return s;
}</pre>
<h3 id="Putting_it_together">Putting it together</h3>
<p>The complete add-on adds a button to Firefox: when the user clicks the button, we ask them to select a file, compute the hash, and log the hash to the console:</p>
<pre class="brush: js">
var {Cc, Ci} = require("chrome");

// return the two-digit hexadecimal code for a byte
function toHexString(charCode) {
  return ("0" + charCode.toString(16)).slice(-2);
}

function md5File(path) {
  var f = Cc["@mozilla.org/file/local;1"]
                  .createInstance(Ci.nsILocalFile);
  f.initWithPath(path);
  var istream = Cc["@mozilla.org/network/file-input-stream;1"]           
                        .createInstance(Ci.nsIFileInputStream);
  // open for reading
  istream.init(f, 0x01, 0444, 0);
  var ch = Cc["@mozilla.org/security/hash;1"]
                   .createInstance(Ci.nsICryptoHash);
  // we want to use the MD5 algorithm
  ch.init(ch.MD5);
  // this tells updateFromStream to read the entire file
  const PR_UINT32_MAX = 0xffffffff;
  ch.updateFromStream(istream, PR_UINT32_MAX);
  // pass false here to get binary data back
  var hash = ch.finish(false);

  // convert the binary hash data to a hex string.
  var s = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
  return s;
}

function promptForFile() {
  var window = require("sdk/window/utils").getMostRecentBrowserWindow();
  const nsIFilePicker = Ci.nsIFilePicker;

  var fp = Cc["@mozilla.org/filepicker;1"]
           .createInstance(nsIFilePicker);
  fp.init(window, "Select a file", nsIFilePicker.modeOpen);
  fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText);

  var rv = fp.show();
  if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace) {
    var file = fp.file;
    // Get the path as string. Note that you usually won't
    // need to work with the string paths.
    var path = fp.file.path;
    // work with returned nsILocalFile...
  }
  return path;
}

require("sdk/ui/button/action").ActionButton({
  id: "show-panel",
  label: "Show Panel",
  icon: {
    "16": "./icon-16.png"
  },
  onClick: function() {
    console.log(md5File(promptForFile()));
  }
});
</pre>
<p>This works , but main.js is now getting longer and its logic is harder to understand. Let's factor the file picker and hashing code into separate modules.</p>
<h2 id="Creating_separate_modules">Creating separate modules</h2>
<h3 id="filepicker.js">filepicker.js</h3>
<p>First create a new file in "lib" called "filepicker.js", and copy the file picker code into this new file, and add the following line at the end:</p>
<p>Next, add the following line somewhere in the new file:</p>
<pre class="brush: js">
exports.promptForFile = promptForFile;
</pre>
<p>This defines the public interface of the new module.</p>
<p>So "filepicker.js" should look like this:</p>
<pre class="brush: js">
var {Cc, Ci} = require("chrome");

function promptForFile() {
&nbsp; var window = require("sdk/window/utils").getMostRecentBrowserWindow();
&nbsp; const nsIFilePicker = Ci.nsIFilePicker;

&nbsp; var fp = Cc["@mozilla.org/filepicker;1"]
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; .createInstance(nsIFilePicker);
&nbsp; fp.init(window, "Select a file", nsIFilePicker.modeOpen);
&nbsp; fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText);

&nbsp; var rv = fp.show();
&nbsp; if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace) {
&nbsp;&nbsp;&nbsp; var file = fp.file;
&nbsp;&nbsp;&nbsp; // Get the path as string. Note that you usually won't
&nbsp;&nbsp;&nbsp; // need to work with the string paths.
&nbsp;&nbsp;&nbsp; var path = fp.file.path;
&nbsp;&nbsp;&nbsp; // work with returned nsILocalFile...
&nbsp; }
&nbsp; return path;
}

exports.promptForFile = promptForFile;
</pre>
<h3 id="md5.js">md5.js</h3>
<p>Next, create another file in "lib", called "md5.js". Copy the hashing code there, and add this line at the end:</p>
<pre class="brush: js">
exports.hashFile = md5File;</pre>
<p>The complete file looks like this:</p>
<pre class="brush: js">
var {Cc, Ci} = require("chrome");
var filepicker = require("./filepicker.js");

// return the two-digit hexadecimal code for a byte
function toHexString(charCode) {
  return ("0" + charCode.toString(16)).slice(-2);
}

function md5File(path) {
  var f = Cc["@mozilla.org/file/local;1"]
                  .createInstance(Ci.nsILocalFile);
  f.initWithPath(path);
  var istream = Cc["@mozilla.org/network/file-input-stream;1"]           
                        .createInstance(Ci.nsIFileInputStream);
  // open for reading
  istream.init(f, 0x01, 0444, 0);
  var ch = Cc["@mozilla.org/security/hash;1"]
                   .createInstance(Ci.nsICryptoHash);
  // we want to use the MD5 algorithm
  ch.init(ch.MD5);
  // this tells updateFromStream to read the entire file
  const PR_UINT32_MAX = 0xffffffff;
  ch.updateFromStream(istream, PR_UINT32_MAX);
  // pass false here to get binary data back
  var hash = ch.finish(false);

  // convert the binary hash data to a hex string.
  var s = [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
  return s;
}

exports.hashFile = md5File;</pre>
<h3 id="main.js">main.js</h3>
<p>Finally, update main.js to import these two new modules and use them:</p>
<pre class="brush: js">
var filepicker = require("./filepicker.js");
var md5 = require("./md5.js");

require("sdk/ui/button/action").ActionButton({
  id: "show-panel",
  label: "Show Panel",
  icon: {
    "16": "./icon-16.png"
  },
  onClick: function() {
    console.log(md5.hashFile(filepicker.promptForFile()));
  }
});</pre>
<p>You can distribute these modules to other developers, too. They can copy them somewhere under the add-on, and include them using <code>require()</code> in the same way.</p>
<h2 id="Learning_More">Learning More</h2>
<p>To see some of the modules people have already developed, see the page of <a href="https://github.com/mozilla/addon-sdk/wiki/Community-developed-modules">community-developed modules</a>. To learn how to use third-party modules in your own code, see the <a href="/en-US/Add-ons/SDK/Tutorials/Add_a_Menu_Item_to_Firefox">tutorial on adding menu items</a>.</p>
Revert to this revision