mozilla

Compare Revisions

Creating Reusable Modules

Change Revisions

Revision 558807:

Revision 558807 by wbamberg on

Revision 558811:

Revision 558811 by wbamberg on

Title:
Creating Reusable Modules
Creating Reusable Modules
Slug:
Mozilla/Add-ons/SDK/Tutorials/Creating_reusable_modules
Mozilla/Add-ons/SDK/Tutorials/Creating_reusable_modules
Content:

Revision 558807
Revision 558811
n65      Firefox has built-in support for hash functions, exposed vin65      Firefox has built-in support for hash functions, exposed vi
>a the nsICryptoHash XPCOM interface, and the <a href="/en-US/docs>a the nsICryptoHash XPCOM interface The <a href="/en-US/docs/XPCO
>/XPCOM_Interface_Reference/nsICryptoHash">documentation page for >M_Interface_Reference/nsICryptoHash">documentation page for that 
>that interface</a> includes an example of calculating an MD5 hash>interface</a> includes an example of calculating an MD5 hash of a
> of a file's contents, give> file's contents, given its path. We can adapt it like this:
66    </p>
67    <p>66    </p>
68      Suppose we want to use the <a href="https://developer.mozil67    <pre class="brush: js">
>la.org/en-US/docs/WebAPI/Using_geolocation">geolocation API built 
> into Firefox</a>. The SDK doesn't provide an API to access geolo 
>cation, but we can <a href="/en-US/Add-ons/SDK/Tutorials/Chrome_A 
>uthority">access the underlying XPCOM API using <code>require("ch 
>rome")</code></a>. 
68var {Cc, Ci} = require("chrome");
69 
70// return the two-digit hexadecimal code for a byte
71function toHexString(charCode) {
72  return ("0" + charCode.toString(16)).slice(-2);
73}
74 
75function md5File(path) {
76  var f = Cc["@mozilla.org/file/local;1"]
77                  .createInstance(Ci.nsILocalFile);
78  f.initWithPath(path);
79  var istream = Cc["@mozilla.org/network/file-input-stream;1"]   
 >        
80                        .createInstance(Ci.nsIFileInputStream);
81  // open for reading
82  istream.init(f, 0x01, 0444, 0);
83  var ch = Cc["@mozilla.org/security/hash;1"]
84                   .createInstance(Ci.nsICryptoHash);
85  // we want to use the MD5 algorithm
86  ch.init(ch.MD5);
87  // this tells updateFromStream to read the entire file
88  const PR_UINT32_MAX = 0xffffffff;
89  ch.updateFromStream(istream, PR_UINT32_MAX);
90  // pass false here to get binary data back
91  var hash = ch.finish(false);
92 
93  // convert the binary hash data to a hex string.
94  var s = [toHexString(hash.charCodeAt(i)) for (i in hash)].join(
 >"");
95  return s;
96}
97</pre>
98    <h3>
99      Putting it together
100    </h3>
69    </p>101    <p>
102      The complete add-on adds a button to Firefox: when the user
 > clicks the button, we ask them to select a file, compute the has
 >h, and log the hash to the console:
70    <p>103    </p>
71      The following add-on adds a <a href="/en-US/Add-ons/SDK/Tut
>orials/Adding_a_Button_to_the_Toolbar">button to the toolbar</a>: 
> when the user clicks the button, it loads the <a href="https://d 
>eveloper.mozilla.org/en-US/docs/XPCOM_Interface_Reference/NsIDOMG 
>eoGeolocation">XPCOM nsIDOMGeoGeolocation</a> object, and retriev 
>es the user's current position: 
72    </p>
n74const {Cc, Ci} = require("chrome");n105var {Cc, Ci} = require("chrome");
75 106 
76// Implement getCurrentPosition by loading the nsIDOMGeoGeolocati107// return the two-digit hexadecimal code for a byte
>on 
77// XPCOM object.108function toHexString(charCode) {
78function getCurrentPosition(callback) {109  return ("0" + charCode.toString(16)).slice(-2);
79  var xpcomGeolocation = Cc["@mozilla.org/geolocation;1"]
80                      .getService(Ci.<code>nsISupports</code>);
81  xpcomGeolocation.getCurrentPosition(callback);
n83 n111 
84var widget = require("sdk/widget").Widget({112function md5File(path) {
85  id: "whereami",113  var f = Cc["@mozilla.org/file/local;1"]
86  label: "Where am I?",114                  .createInstance(Ci.nsILocalFile);
87  contentURL: "http://www.mozilla.org/favicon.ico",115  f.initWithPath(path);
116  var istream = Cc["@mozilla.org/network/file-input-stream;1"]   
 >        
117                        .createInstance(Ci.nsIFileInputStream);
118  // open for reading
119  istream.init(f, 0x01, 0444, 0);
120  var ch = Cc["@mozilla.org/security/hash;1"]
121                   .createInstance(Ci.nsICryptoHash);
122  // we want to use the MD5 algorithm
123  ch.init(ch.MD5);
124  // this tells updateFromStream to read the entire file
125  const PR_UINT32_MAX = 0xffffffff;
126  ch.updateFromStream(istream, PR_UINT32_MAX);
127  // pass false here to get binary data back
128  var hash = ch.finish(false);
129 
130  // convert the binary hash data to a hex string.
131  var s = [toHexString(hash.charCodeAt(i)) for (i in hash)].join(
 >"");
132  return s;
133}
134 
135function promptForFile() {
136  var window = require("sdk/window/utils").getMostRecentBrowserWi
 >ndow();
137  const nsIFilePicker = Ci.nsIFilePicker;
138 
139  var fp = Cc["@mozilla.org/filepicker;1"]
140           .createInstance(nsIFilePicker);
141  fp.init(window, "Select a file", nsIFilePicker.modeOpen);
142  fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filter
 >Text);
143 
144  var rv = fp.show();
145  if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnR
 >eplace) {
146    var file = fp.file;
147    // Get the path as string. Note that you usually won't
148    // need to work with the string paths.
149    var path = fp.file.path;
150    // work with returned nsILocalFile...
151  }
152  return path;
153}
154 
155require("sdk/ui/button/action").ActionButton({
156  id: "show-panel",
157  label: "Show Panel",
158  icon: {
159    "16": "./icon-16.png"
160  },
n89    getCurrentPosition(function(position) {n162    console.log(md5File(promptForFile()));
90      console.log("latitude: ", position.coords.latitude);
91      console.log("longitude: ", position.coords.longitude);
92    });
n97      Try it out:n167      This works , but main.js is now getting longer and its logi
 >c is harder to understand. Let's factor the file picker and hashi
 >ng code into separate modules.
98    </p>
99    <ul>
100      <li>create a new directory called "whereami" and navigate t
>o it 
101      </li>
102      <li>execute <code>cfx init</code>
103      </li>
104      <li>open "lib/main.js" and add the code above
105      </li>
106      <li>execute <code>cfx run</code>, then <code>cfx run</code>
> again 
107      </li>
108    </ul>
109    <p>
110      You should see a button added to the "Add-on Bar" at the bo
>ttom of the browser window: 
111    </p>
112    <p>
113      <img alt="" src="https://mdn.mozillademos.org/files/6541/wi
>dget-mozilla.png" style="display: block; margin-left: auto; margi 
>n-right: auto;"> 
114    </p>
115    <p>
116      Click the button, and after a short delay you should see ou
>tput like this in the console: 
117    </p>
118    <pre>
119info: latitude:  29.45799999
120info: longitude:  93.0785269
121</pre>
122    <p>
123      So far, so good. But the geolocation guide on MDN tells us 
>that we must <a href="https://developer.mozilla.org/en-US/docs/We 
>bAPI/Using_geolocation#Prompting_for_permission">ask the user for 
> permission</a> before using the API. 
124    </p>
125    <p>
126      So we'll extend the add-on to include an adapted version of
> the code in that MDN page: 
127    </p>
128    <pre class="brush: js">
129var activeBrowserWindow = require("sdk/window/utils").getMostRece
>ntBrowserWindow(); 
130var {Cc, Ci} = require("chrome");
131 
132// Ask the user to confirm that they want to share their location
>. 
133// If they agree, call the geolocation function, passing the in t
>he 
134// callback. Otherwise, call the callback with an error message.
135function getCurrentPositionWithCheck(callback) {
136  let pref = "extensions.whereami.allowGeolocation";
137  let message = "whereami Add-on wants to know your location."
138  let branch = Cc["@mozilla.org/preferences-service;1"]
139               .getService(Ci.nsIPrefBranch2);
140  if (branch.getPrefType(pref) === branch.PREF_STRING) {
141    switch (branch.getCharPref(pref)) {
142    case "always":
143      return getCurrentPosition(callback);
144    case "never":
145      return callback(null);
146    }
147  }
148  let done = false;
149 
150function remember(value, result) {
151    return function () {
152      done = true;
153      branch.setCharPref(pref, value);
154      if (result) {
155        getCurrentPosition(callback);
156      }
157      else {
158        callback(null);
159      }
160    }
161  }
162 
163let self = activeBrowserWindow.PopupNotifications.show(
164               activeBrowserWindow.gBrowser.selectedBrowser,
165               "geolocation",
166               message,
167               "geo-notification-icon",
168    {
169      label: "Share Location",
170      accessKey: "S",
171      callback: function (notification) {
172        done = true;
173        getCurrentPosition(callback);
174      }
175    }, [
176      {
177        label: "Always Share",
178        accessKey: "A",
179        callback: remember("always", true)
180      },
181      {
182        label: "Never Share",
183        accessKey: "N",
184        callback: remember("never", false)
185      }
186    ], {
187      eventCallback: function (event) {
188        if (event === "dismissed") {
189          if (!done)
190            callback(null);
191          done = true;
192          PopupNotifications.remove(self);
193        }
194      },
195      persistWhileVisible: true
196    });
197}
198 
199// Implement getCurrentPosition by loading the nsIDOMGeoGeolocati
>on 
200// XPCOM object.
201function getCurrentPosition(callback) {
202  var xpcomGeolocation = Cc["@mozilla.org/geolocation;1"]
203                      .getService(Ci.nsIDOMGeoGeolocation);
204  xpcomGeolocation.getCurrentPosition(callback);
205}
206 
207var widget = require("sdk/widget").Widget({
208  id: "whereami",
209  label: "Where am I?",
210  contentURL: "http://www.mozilla.org/favicon.ico",
211  onClick: function() {
212    getCurrentPositionWithCheck(function(position) {
213      if (!position) {
214        console.log("The user denied access to geolocation.");
215      }
216      else {
217       console.log("latitude: ", position.coords.latitude);
218       console.log("longitude: ", position.coords.longitude);
219      }
220    });
221  }
222});
223</pre>
224    <p>
225      This works fine: when we click the button, we get a notific
>ation box asking for permission, and depending on our choice the  
>add-on logs either the position or an error message. 
226    </p>
227    <p>
228      But the code is now somewhat long and complex, and if we wa
>nt to do much more in the add-on, it will be hard to maintain. So 
> let's split the geolocation code into a separate module. 
n231      Creating a Separate Modulen170      Creating separate modules
n234      Create <code>geolocation.js</code>n173      filepicker.js
235    </h3>
236    <p>174    </h3>
237      First create a new file in "lib" called "geolocation.js", a175    <p>
>nd copy everything except the widget code into this new file. 
176      First create a new file in "lib" called "filepicker.js", an
 >d copy the file picker code into this new file, and add the follo
 >wing line at the end:
n243exports.getCurrentPosition = getCurrentPositionWithCheck;n182exports.promptForFile = promptForFile;
n245    <p>n
246      This defines the public interface of the new module. We exp
>ort a single function to prompt the user for permission and get t 
>he current position if they agree. 
247    </p>184    <p>
185      This defines the public interface of the new module.
248    <p>186    </p>
249      So "geolocation.js" should look like this:
250    </p>187    <p>
188      So "filepicker.js" should look like this:
189    </p>
nn191var {Cc, Ci} = require("chrome");
192 
193function promptForFile() {
252var activeBrowserWindow = require("sdk/window/utils").getMostRece194  var window = require("sdk/window/utils").getMostRecentBrowserWi
>ntBrowserWindow();>ndow();
253var {Cc, Ci} = require("chrome");195  const nsIFilePicker = Ci.nsIFilePicker;
254 196 
255// Ask the user to confirm that they want to share their location197  var fp = Cc["@mozilla.org/filepicker;1"]
>. 
256// If they agree, call the geolocation function, passing the in t198           .createInstance(nsIFilePicker);
>he 
257// callback. Otherwise, call the callback with an error message.199  fp.init(window, "Select a file", nsIFilePicker.modeOpen);
258function getCurrentPositionWithCheck(callback) {200  fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filter
 >Text);
259  let pref = "extensions.whereami.allowGeolocation";201 
260  let message = "whereami Add-on wants to know your location."202  var rv = fp.show();
261  let branch = Cc["@mozilla.org/preferences-service;1"]203  if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnR
 >eplace) {
262               .getService(Ci.nsIPrefBranch2);204    var file = fp.file;
263  if (branch.getPrefType(pref) === branch.PREF_STRING) {205    // Get the path as string. Note that you usually won't
264    switch (branch.getCharPref(pref)) {206    // need to work with the string paths.
265    case "always":207    var path = fp.file.path;
266      return getCurrentPosition(callback);208    // work with returned nsILocalFile...
267    case "never":
268      return callback(null);
269    }209  }
270  }210  return path;
271  let done = false;
272 
273function remember(value, result) {
274    return function () {
275      done = true;
276      branch.setCharPref(pref, value);
277      if (result) {
278        getCurrentPosition(callback);
279      }
280      else {
281        callback(null);
282      }
283    }
284  }
285 
286let self = activeBrowserWindow.PopupNotifications.show(
287               activeBrowserWindow.gBrowser.selectedBrowser,
288               "geolocation",
289               message,
290               "geo-notification-icon",
291    {
292      label: "Share Location",
293      accessKey: "S",
294      callback: function (notification) {
295        done = true;
296        getCurrentPosition(callback);
297      }
298    }, [
299      {
300        label: "Always Share",
301        accessKey: "A",
302        callback: remember("always", true)
303      },
304      {
305        label: "Never Share",
306        accessKey: "N",
307        callback: remember("never", false)
308      }
309    ], {
310      eventCallback: function (event) {
311        if (event === "dismissed") {
312          if (!done)
313            callback(null);
314          done = true;
315          PopupNotifications.remove(self);
316        }
317      },
318      persistWhileVisible: true
319    });
n321 n212 
322// Implement getCurrentPosition by loading the nsIDOMGeoGeolocati213exports.promptForFile = promptForFile;
>on 
323// XPCOM object.
324function getCurrentPosition(callback) {
325  var xpcomGeolocation = Cc["@mozilla.org/geolocation;1"]
326                      .getService(Ci.nsIDOMGeoGeolocation);
327  xpcomGeolocation.getCurrentPosition(callback);
328}
329 
330exports.getCurrentPosition = getCurrentPositionWithCheck;
n333      Update <code>main.js</code>n216      md5.js
217    </h3>
218    <p>
219      Next, create another file in "lib", called "md5.js". Copy t
 >he hashing code there, and add this line at the end:
220    </p>
221    <pre class="brush: js">
222exports.hashFile = md5File;
223</pre>
224    <p>
225      The complete file looks like this:
226    </p>
227    <pre class="brush: js">
228var {Cc, Ci} = require("chrome");
229var filepicker = require("./filepicker.js");
230 
231// return the two-digit hexadecimal code for a byte
232function toHexString(charCode) {
233  return ("0" + charCode.toString(16)).slice(-2);
234}
235 
236function md5File(path) {
237  var f = Cc["@mozilla.org/file/local;1"]
238                  .createInstance(Ci.nsILocalFile);
239  f.initWithPath(path);
240  var istream = Cc["@mozilla.org/network/file-input-stream;1"]   
 >        
241                        .createInstance(Ci.nsIFileInputStream);
242  // open for reading
243  istream.init(f, 0x01, 0444, 0);
244  var ch = Cc["@mozilla.org/security/hash;1"]
245                   .createInstance(Ci.nsICryptoHash);
246  // we want to use the MD5 algorithm
247  ch.init(ch.MD5);
248  // this tells updateFromStream to read the entire file
249  const PR_UINT32_MAX = 0xffffffff;
250  ch.updateFromStream(istream, PR_UINT32_MAX);
251  // pass false here to get binary data back
252  var hash = ch.finish(false);
253 
254  // convert the binary hash data to a hex string.
255  var s = [toHexString(hash.charCodeAt(i)) for (i in hash)].join(
 >"");
256  return s;
257}
258 
259exports.hashFile = md5File;
260</pre>
334    </h3>261    <h3>
262      main.js
335    <p>263    </h3>
336      Finally, update "main.js". First add a line to import the n
>ew module: 
337    </p>264    <p>
338    <div>265      Finally, update main.js to import these two new modules and
 > use them:
339      <div>
340        <div>
341          <div class="container">
342            <pre class="line number1 index0 alt2">
343<code class="js keyword">var</code> <code class="js plain">geoloc
>ation = require(</code><code class="js string">"./geolocation"</c 
>ode><code class="brush: js">);</code> 
344</pre>
345          </div>
346        </div>
347      </div>
348    </div>
349    <p>266    </p>
350      When importing modules that are not SDK built in modules, i
>t's a good idea to specify the path to the module explicitly like 
> this, rather than relying on the module loader to find the modul 
>e you intended. 
351    </p>
352    <p>
353      Edit the widget's call to <code>getCurrentPositionWithCheck
>()</code> so it calls the geolocation module's <code>getCurrentPo 
>sition()</code> function instead: 
354    </p>
n356geolocation.getCurrentPosition(function(position) {n268var filepicker = require("./filepicker.js");
357  if (!position) {269var md5 = require("./md5.js");
358</pre>270 
359    <p>271require("sdk/ui/button/action").ActionButton({
360      Now "main.js" should look like this:272  id: "show-panel",
361    </p>273  label: "Show Panel",
362    <pre class="brush: js">274  icon: {
363var geolocation = require("./geolocation");275    "16": "./icon-16.png"
364 276  },
365var widget = require("sdk/widget").Widget({
366  id: "whereami",
367  label: "Where am I?",
368  contentURL: "http://www.mozilla.org/favicon.ico",
n370    geolocation.getCurrentPosition(function(position) {n278    console.log(md5.hashFile(filepicker.promptForFile()));
371      if (!position) {
372        console.log("The user denied access to geolocation.");
373      }
374      else {
375       console.log("latitude: ", position.coords.latitude);
376       console.log("longitude: ", position.coords.longitude);
377      }
378    });
t382    <h2 id="Packaging_the_Geolocation_Module">t
383      Packaging the Geolocation Module
384    </h2>
385    <p>
386      So far, this is a useful technique for structuring your add
>-on. But you can also package and distribute modules independentl 
>y of your add-on: then any other add-on developer can download yo 
>ur module and use it in exactly the same way they use the SDK's b 
>uilt-in modules. 
387    </p>282    <p>
388    <h3 id="Code_Changes">283      You can distribute these modules to other developers, too. 
 >They can copy them somewhere under the add-on, and include them u
 >sing <code>require()</code> in the same way.
389      Code Changes
390    </h3>
391    <p>
392      First we'll make a couple of changes to the code. At the mo
>ment the message displayed in the prompt and the name of the pref 
>erence used to store the user's choice are hardcoded: 
393    </p>
394    <pre class="brush: js">
395let pref = "extensions.whereami.allowGeolocation";
396let message = "whereami Add-on wants to know your location."
397</pre>
398    <p>
399      Instead we'll use the <a href="/en-US/Add-ons/SDK/High-Leve
>l_APIs/self"><code>self</code></a> module to ensure that they are 
> specific to the add-on: 
400    </p>
401    <pre class="brush: js">
402var addonName = require("sdk/self").name;
403var addonId = require("sdk/self").id;
404let pref = "extensions." + addonId + ".allowGeolocation";
405let message = addonName + " Add-on wants to know your location."
406</pre>
407    <h3 id="Repackaging">
408      Repackaging
409    </h3>
410    <p>
411      Next we'll repackage the geolocation module.
412    </p>
413    <ul>
414      <li>create a new directory called "geolocation", and run <c
>ode>cfx init</code> in it. 
415      </li>
416      <li>delete the "main.js" that <code>cfx</code> generated, a
>nd copy "geolocation.js" there instead. 
417      </li>
418    </ul>
419    <h3 id="Editing_.22package.json.22">
420      Editing "package.json"
421    </h3>
422    <p>
423      The "package.json" file in your package's root directory co
>ntains metadata for your package. See the <a href="/en-US/Add-ons 
>/SDK/Tools/package_json">package specification</a> for full detai 
>ls. If you intend to distribute the package, this is a good place 
> to add your name as the author, choose a distribution license, a 
>nd so on. 

Back to History