Access Add-On SDK 'main' from XUL

2020-08-03 09:32发布

问题:

In my add-on, I'm using XUL to display dialog windows because I can customize their appearance to suit the add-on's general style (like a custom titlebar).

Using the migration guide, I'm able to do this easily. The thing is, I would like to call certain functions in the add-on's main module from the XUL dialog.

After a bit of searching I found the loader module, which seems to be able to do exactly what I want. But, I'm experiencing trouble in using it to access the main module.

First, I tried the obvious approach as mentioned in the documentation;

xul_dialog.js:

let {Loader} = Components.utils.import('resource://gre/modules/commonjs/toolkit/loader.js');
let loader = Loader.Loader({
    paths: {
        'toolkit/': 'resource://gre/modules/commonjs/toolkit/',
        '': 'resource:///modules/',
        './': 'resource://<my-addon-name>/root/'
    }
});

let main = Loader.main(loader, './main');

I got an error that './main' was not found at resource://<my-addon-name>/root/. Figuring that I was using the incorrect paths, I experimented a bit until I could remove all path associated errors.

xul_dialog.js:

let {Loader} = Components.utils.import('resource://gre/modules/commonjs/toolkit/loader.js');
let loader = Loader.Loader({
    paths: {
        'toolkit/': 'resource://gre/modules/commonjs/toolkit/',
        '': 'resource://gre/modules/commonjs/',
        './': 'resource://<my-addon-id>-at-jetpack/<my-addon-name>/lib/'
    }
});

let main = Loader.main(loader, './main');

This time I got a rather confusing error at loader.js, line 279.

Components is not available in this context.
Functionality provided by Components may be available in an SDK
module: https://jetpack.mozillalabs.com/sdk/latest/docs/

However, if you still need to import Components, you may use the
`chrome` module's properties for shortcuts to Component properties:

Shortcuts:
    Cc = Components.classes
    Ci = Components.interfaces
    Cu = Components.utils
    CC = Components.Constructor
Example:
    let { Cc, Ci } = require('chrome');

I get the same error when I use Loader.Require(loader, {id: './main'}) instead of Loader.main. I even tried passing Components as globals when instantiating the loader, but without much luck.

I'm fairly certain that I'm doing a lot of things wrong. I don't understand why I'm getting the error, even after spending quite a bit of time in loader.js. Plus, I also think that there would be a better alternative than having to use the add-on id for the path to main.js; hard-coding it like that doesn't seem right.

Any help would be really appreciated.

回答1:

What you have to do is to find a specific instance of Loader, not create a new one.

At main.js

const { id, name, prefixURI } = require("@loader/options");
//pass these to the XUL dialog

At xul.js (or whatever is the name of the xul dialog script)

Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm");
var extensionscope = XPIProvider.bootstrapScopes[id];
var mainjssandbox = extensionscope.loader.sandboxes[prefixURI + name + "/lib/main.js"];

Assuming there is a foo function at main.js, you can call it like

 mainjssandbox.foo();

Of course don't expect things to work as if XUL and Add-on SDK actually blended into one thing.



回答2:

If it is your XUL dialog that should interact with your add-on, then please don't use the Loader stuff and in particular do not go the XPIProvider.bootstrapScopes @paa suggested. While this might work (for now), it should be noted that it relies on tons of implementation details that are subject to change at any point making this solution extremely fragile.

Instead there are a couple of other options (not an exhaustive list):

  • If the SDK part opens the windows, you may use .openDialog which supports passing arguments to the created window, and these arguments can even be objects and functions. Also, you can have the window dispatch (custom) events, and which your SDK part can listen to by calling addEventListener on the window the .openDialog call returns.
  • If the window is created from somewhere else (e.g. from the AddonManager because of em:optionsURL) then the nsIObserverService is another way to communicate. The window could e.g. .notifyObservers on DOMContentLoaded containing a reference to itself. The SDK parts would just have to observe such notifications by addObserver.
  • Another way, a bit hacky but working, is the SDK part listening to new windows via nsIWindowWatcher.registerNotification and then injecting some API into e.g. browser.xul windows by XPCNativeWrapper.unwrap(subject.QueryInterface(Ci.nsIDOMWindow)).myAddonAPI = something.

Be sure to handle unloading or your add-on well - it is still restartless -, i.e. reverse any changes you've done and remove any observers and so on again.

If you want to interact with SDK addons not created by you, then the XPIProvider route might be the only feasible. But still, it might be worth contacting the add-on author first asking for the addition of some public API instead of hacking deep into the AddonManager and SDK loader internals.

PS:

Considering passing require or the global scope to your window via openDialog if you want to. Get the global scope by placing this into your main.js:

const globalScope = this;