JavaScript对象管理

Chrome JavaScript

In this section we'll look into how to handle JavaScript data effectively, beginning with chrome code, in ways which will prevent pollution of shared namespaces and conflicts with other add-ons resulting from such global namespace pollution.

The first step to good JavaScript object management is having a namespace, or a JavaScript object that contains our code and data, that you know will not conflict with Firefox code or other extensions. Namespace declaration is best located in a file of its own, so that you have this one JS file that should be included in all of your XUL files.

We'll be using the placeholder 〈Namespace〉 below. This needs to be replaced with an identifier name which is unique to your add-on. If your add-on is called Sergeant Pepper, for instance, then SgtPepper would be a good namespace name.

/**
 * 〈Namespace〉 namespace.
 */
if (typeof 〈Namespace〉 == "undefined") {
  var 〈Namespace〉 = {};
};
Note: The naming standard that we normally follow is that the first part of the namespace corresponds to the development group (or company), and the second to the specific project. However, most extensions are small projects by individuals, so these examples follow a more practical approach of having just one namespace with the project name.

Notice how the 〈Namespace〉 namespace is declared using var. We need the namespace to be a global object that it can be used everywhere in the window chrome.

You can include functions in any namespace, since namespaces are just regular JS objects. That should come in handy when you have general utility functions or properties that you want to use across all objects within the namespace. For instance, there are frequently used XPCOM services such as the Observer service that can be included as members in the namespace:

/**
 * 〈Namespace〉 namespace.
 */
if (typeof 〈Namespace〉 == "undefined") {
  var 〈Namespace〉 = {
    /**
     * Initializes this object.
     */
    init : function() {
      this.obsService =
        Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
    }
  };

  〈Namespace〉.init();
};

JS objects can also be treated as string-indexed arrays:

// equivalent.
〈Namespace〉.Hello = {};
〈Namespace〉["Hello"] = {};

// equivalent.
〈Namespace〉.Hello.init();
〈Namespace〉.Hello["init"]();

This is very useful in cases where you have to set attributes or functions with dynamically generated names. It's one of the funky properties of JavaScript: all objects are nothing more than name / value mappings. You can add or replace functions and attributes to any Javascript object, at any moment you want. This is an odd, but powerful feature that comes in handy at times when things get complicated. For instance, you could replace a method in any object in the Firefox chrome, so that it behaves differently than how it normally does. This should be a last resort option, but it is very useful at times.

You usually need only one JS file to control a XUL window, since the code required is normally not that much. If you have complex behavior that requires too much code, look for ways to divide it into multiple objects and files. You can include as many scripts in a XUL window as you need.

To initialize your chrome objects, it's usually better to run the initialization code from the "load" event handler for the window. The load event is fired after the DOM on the window has loaded completely, but before it's displayed to the user. This allows you to manipulate and possibly change elements in the window without the user noticing the changes.

/**
 * Controls the browser overlay for the Hello World extension.
 */
〈Namespace〉.BrowserOverlay = {
  /**
   * Initializes this object.
   */
  init : function(aEvent) {
    this._stringBundle = document.getElementById("xulschoolhello-string-bundle");
    // you can make changes to the window DOM here.
  }
  // more stuff
};

window.addEventListener(
  "load", function() { 〈Namespace〉.BrowserOverlay.init(); }, false);

There are some things you can't (or shouldn't) do inside load handlers, such as closing the window being loaded, or opening new windows, alerts or dialogs. The window has to finish loading before it can do any of these things. They are bad UI practices anyway and you should avoid them. If you really need to do something like this anyway, one way to do it is to have a timeout execute the code after a delay:

init : function(aEvent) {
  let that = this;

  this._stringBundle = document.getElementById("xs-hw-string-bundle");
  window.setTimeout(
    function() { 
      window.alert(that._stringBundle.getString("xulschoolhello.greeting.label")); }, 0);
}

The setTimeout function executes the function in the first parameter, after a delay in miliseconds specified by the second parameter. In this case we set the delay to 0, which means the function should be executed as soon as possible. Firefox has a minimum delay of 10-15ms (taken from this blog post), so it won't really run instantly. It is more than enough to let the window finish its load.

Use window.setTimeout and window.setInterval to control timed code execution. In case you're using JavaScript Code Modules or XPCOM objects, where a window object is not readily available, use an nsITimer instead. 
This post suggests a way to achieve a true zero ms timeout, as a simple way to achieve parallelism in JS code.

Notice the way we send callback functions as parameters, and the use of an alternate reference for this which we always name that. This is all necessary due to a JavaScript feature (quirk would be a better word for it) called Method Binding. The consequence of doing this wrong is to have a this reference that doesn't do what you expected it to do. There are a few workarounds for this, and we use the ones we have found to be the most elegant and clear to read.

The general guideline we follow is this: whenever you have to set a callback function parameter, wrap it in an anonymous function. That is, something like function() { /* your code, usually a single function call. */ }. If you have to use a reference to this inside the function, declare a variable called that that equals this, and use that in the anonymous function.

JavaScript has a host of features that make it extremely flexible, but it also has some disadvantages, as it is not as strict as other languages, such as Java. A clear example of this is the fact that there are no private or public keywords that allow you to protect object members. As a alternative for this, a naming standard is frequently used to distinguish private and public members. There's no scope enforcement whatsoever, but this standard give others the chance to "play nice" and don't use private members.

Use "_" at the beginning of private attributes and methods in JS objects. For example: _stringBundle, _createUserNode().

Exercise

Here's a short exercise to test a particular aspect of the chrome. Modify the Hello World extension so that the message says how many times it has been displayed. The message could say something like "Hello! This message has been shown 5 times." Keep the counter as a variable in the BrowserOverlay object, and increment it every time the message is going to be shown.

Once you have this working right, try the following: open the message a few times, so that the number increments. Now open a new window and display the message from the new window. What do you think will happen? What will the count be this time?

You probably didn't expect this, but the count was reset in the new window. Each window keeps its own counter, and now the extension is not behaving as expected. This is a fundamental lesson: the chrome is not global, it's window-specific. All of your scripts and objects are replicated for each window, and they work independently from each other. This is an issue that is very easy to overlook, since most Firefox users, specially power users, have a single window open at all times. You have to make sure you test your extension with multiple windows open; never assume everything will work the same as with a single window.

Now, in most cases you'll need to coordinate data in a way that it is consistent for all open Firefox windows. There are several ways in which you can do this. Preferences is one of them, and they are covered in another section of this tutorial. Two other ways are JavaScript Code Modules (Firefox 3 and above), and XPCOM.

JavaScript Code Modules

JavaScript Code Modules (also known as JSM) are new to Firefox 3, and they're the best tool for keeping everything in sync between windows. They're very simple to set up. The first thing you need to do is add an entry in the chrome.manifest file:

resource  xulschoolhello     modules/

Javascript code modules are accessed with the resource protocol, which is very similar to the chrome protocol. Just like with the chrome, we define the package name and then a path. To keep things simple, just locate the JSM files in a modules directory under the root of our project. In order to access a file messageCount.js in this directory, the URL would be:

resource://xulschoolhello/messageCount.js

Code modules are regular JS files, so there's nothing new in regards to naming or file types. Mozilla has adopted a standard of using the extension .jsm for these files, but they say .js is fine as well. To keep things simple, specially regarding code editors and default file associations in the developer's system, we have decided to stick with .js.

Download this version of the Hello World project with JSM to see the changes you need to make to the build system in order to include the files in the modules folder. They are minimal, and we add a very small Makefile.in file in the modules directory, just to keep everything separated and organized.

With the setup out of the way, let's get to it. What are JavaScript Code Modules?

A JavaScript Code Module is a regular JS file that specifies which of the declared elements in it are public. All module files should begin with a declaration like this:

var EXPORTED_SYMBOLS = ["〈ModuleNamespace〉"];

EXPORTED_SYMBOLS is a special identifier that tells Firefox that this file is only publishing the object named 〈ModuleNamespace〉. Several objects, functions and variables can be declared on this file, but the only object visible from the outside will be 〈ModuleNamespace〉, which is a namespace in our case. Because of namespacing, we don't need to worry much about what to export, usually we just need the namespace object. All of the objects inside of it are exported as well, since they are members of the 〈ModuleNamespace〉 object.

Module files can be imported to a chrome script or to other code modules with the following line:

Components.utils.import("resource://xulschoolhello/messageCount.js");
When using Components.utils.import, code modules must be loaded using a file: or resource: URL pointing to a file on the disk. In particular, chrome: URLs (even those that point to a file outside a jar archive) are not valid.

To get a better idea, let's look at the code of the modified Hello World example. We have defined two files, one to declare namespaces and another one for the message count functionality mentioned in the previous exercise.

Here again we're using a placeholder, 〈ModuleNamespace〉, for the identifier name that you'll need to choose.

var EXPORTED_SYMBOLS = [ "〈ModuleNamespace〉" ];

const { classes: Cc, interfaces: Ci } = Components;

/**
 * 〈ModuleNamespace〉 namespace.
 */
var 〈ModuleNamespace〉 = {};

This should all be familiar enough. We're declaring the namespace we'll use at the module level. We need a separate namespace for the chrome because the chrome namespace objects are repeated for each window, while the module namespace objects are unique for all windows. Setting window-specific data on code modules will lead to nothing but problems, so be careful when deciding what should be chrome and what shouldn't be. We needn't test for the pre-existence of our namespace object here, as modules are given their own namespace.

The 2 declared constants above are used to reduce code size. We frequently need to use XPCOM components in our code, so instead of doing this:

this.obsService = 
  Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService);

It's better to do this:

this.obsService = 
  Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);

These 2 constants don't need to be defined in the overlay because they are already defined globally in the browser.js file in Firefox. We only need to define them when we're making windows of our own, or when we're working with code outside of the chrome (or porting your code to SeaMonkey, which doesn't have those constants declared in the main window).

Include the Cc and Ci constants in all XUL windows that are not overlays, all JSM files, and all XPCOM components (see further ahead). Do this even if you don't need them now. It's better to just make a habit out of it.

This is a point that is worth highlighting: modules work outside of the window scope. Unlike scripts in the chrome, modules don't have access to objects such as window, document, or other global functions, such as openUILink. These are all UI components and UI operations anyway, so they are better done in the chrome.

As a general guideline, we keep all of our business logic in JSM, available through service objects, and chrome scripts are limited to handle presentation logic.

We handle most of our code through static objects, singleton objects that don't require instantiation. But it is sometimes necessary to define classes and be able to create multiple instances. Common cases include interacting with a local database or a remote API. Data will often be translated into arrays of entities, and those are better represented through classes. One way to define a class is as follows:

/**
 * User class. Represents a Hello World user (whatever that may be).
 */
〈ModuleNamespace〉.User = function(aName, aURL) {
  this._name = aName;
  this._url = aURL;
};

/**
 * User class methods.
 */
〈ModuleNamespace〉.User.prototype = {
  /* The name of the user. */

  _name : null,
  /* The URL of the user. */

  _url : null,

  /**
   * Gets the user name.
   * @return the user name.
   */
  get name() {
    return this._name;
  },

  /**
   * Gets the user URL.
   * @return the user URL.
   */
  get url() {
    return this._url;
  }
};

In this example we defined a fictitious User class for the Hello World extension. Using the function keyword to define a class is odd, but this is just the JavaScript way: functions are also objects. The definition of the class acts as a constructor as well, and then you can define all other members using the prototype attribute. In this case we defined "getter" properties for the name and url members. This way we have immutable instances of our class. Well, only if consumers of the class play nice and don't change anything they shouldn't.

Creating an instance and using it is simple enough:

let user = new 〈ModuleNamespace〉.User("Pete", "http://example.com/pete");

window.alert(user.name);

This is something you can do with JS in general. You can use it in JSM, chrome, even on regular web pages. Since entities tend to be used all throughout an application, we think that having those classes defined at the module level is the best approach.

JSM is the best solution to handle objects that are window-independent. In the following section we'll discuss XPCOM, which is an older alternative to JSM and one of the foundations of Mozilla applications. You shouldn't skip that section because there are many common situations in extension development where you'll have to use XPCOM, maybe even implement XPCOM components of your own.

This tutorial was kindly donated to Mozilla by Appcoast.

附件

文件 大小 日期 附加者为
HelloWorld3.zip
8717 字节 2011-04-15 22:02:39 Jorge.villalobos
dashboard
75087 字节 2013-04-22 15:34:47 wbamberg

Document Tags and Contributors

Contributors to this page: du.wei
最后编辑者: du.wei,