One of the challenges of converting Firefox features and add-ons to multiprocess Firefox involves in-content pages. These pages (like about:addons) display in a tab but for now still render in the main process. There can be many reasons for wanting to move these into the content process, particularly if the page interacts with web content in any way but more generally just to keep the main UI as responsive as possible.
This raises the problem of how the page communicates back to the main process to get information and trigger actions. Commonly this is done with a frame script listening for DOM events from inside the page, performing the necessary message passing and then passing results back to the page. However, this middleman approach has problems. The frame code is maintained separately from the page code leading to three different pieces of code handling behavior for the page. Add-ons also have to be careful to choose unique names for messages to avoid conflicting with other add-ons and Firefox code.
RemotePageManager
is an API designed to simplify this. It acts as the middleman between code in the main process and code in the page without needing to write custom code in a frame script. Code in the main process registers URLs that it is interested in. Whenever a frame loads a page with that URL, a pseudo message manager is created to allow message passing between just that page and the code in the main process. Code running in the page itself can access the message manager directly without needing to use a frame script at all.
RemotePageManager
RemotePageManager
is the low-level way to interact with in-content pages. It includes the following methods:
addRemotePageListener(url, callback)
- Registers interest in a URL. Whenever a new page loads at that URL callback is called and passed the message channel to use to communicate with the page. Only one callback can be registered per URL.
removeRemotePageListener(url)
- Unregisters interest in a URL. The callback passed above will not be called again.
RemotePages
RemotePages
is a higher-level option. As well as the methods listed here it also has the message channel methods which will send messages to, and receive messages from, every page currently loaded at the URL.
RemotePages(url)
- Creates a new
RemotePages
instance for a URL. destroy()
- Destroys the instance.
Message channel methods
All of these methods are available directly in the page, on RemotePages
instances, or on the channel passed to callbacks registered with RemotePageManager.addRemotePageListener
.
sendAsyncMessage(name, data)
- Sends a named message to the other side of the channel.
data
is copied as a structured clone. addMessageListener(name, listener)
- Adds a listener for a named message.
listener
will be called when the other side of the channel sends a message forname
.listener
will be passed an object with the propertiestarget
,name
, anddata
. removeMessageListener(name, listener)
- Removed a listener for a named message.
Special messages
As well as any messages that Firefox or add-on code sends through the message channels, some special messages will be sent. Generally any message name starting with "RemotePage:" should be considered reserved for future use.
- RemotePage:Init
- Sent to a
RemotePages
instance when a new page is loaded with the matching URL. Thetarget
property of the object passed to any listener is a message channel for just that page. - RemotePage:Load
- Sent when the load event for a matching page in the content process is fired.
- RemotePage.Unload
- Sent when a matching page in the content process is unloaded.
Low level example
This example waits for a page to load and then passes it some information.
// code running in the main process somewhere Components.utils.import("resource://gre/modules/RemotePageManager.jsm"); RemotePageManager.addRemotePageListener("about:foo", (channel) => { // Wait for page load here to be sure the page has had chance to register for this message channel.addMessageListener("RemotePage:Load", function() { channel.sendAsyncMessage("MyMessage", { somedata: 42 }); }); });
Here is the code that runs in the page. Note that it has direct access to addMessageListener
(as well as the other message channel methods).
// code running in the content webpage addMessageListener("MyMessage", function(msg) { alert(msg.data.somedata); });
High level example
The code above uses a callback that is called every time a page is loaded. In reality it is going to be more common for code in the main process to simply respond to requests from the page and want to update all pages at once. RemotePages
is more suited to this:
// code running in the main process somewhere Components.utils.import("resource://gre/modules/Preferences.jsm"); Components.utils.import("resource://gre/modules/RemotePageManager.jsm"); let listener = new RemotePages("about:bar"); // Listens to messages from all current and future pages listener.addMessageListener("GetLabel", ({ target }) => { // target is the channel for just the page that sent this message target.sendAsyncMessage("SetLabel", Preferences.get("extensions.label.text")); }); Preferences.observe("extensions.label.text", (newLabel) => { // Sends messages to all currently open pages listener.sendAsyncMessage("SetLabel", newLabel); }); listener.addMessageListener("ButtonClicked", () => { // Do something here });
// code running in the content webpage var button = document.getElementById("button"); var label = document.getElementById("label"); sendAsyncMessage("GetLabel"); addMessageListener("SetLabel", function(msg) { label.textContent = msg.data; }); function buttonClicked() { sendAsyncMessage("ButtonClicked"); } button.addEventListener("click", buttonClicked, false);