The Chrome Extension Skeleton: Messaging System

When writing our Chrome Extension Skeleton, we spent some time thinking about what our extensions have in common and which shared code they would most benefit from. Even though each extension is unique, under the hood:

  1. There is some shared state maintained by the extension.
  2. Various extension pieces communicate with each other.

shutterstock_111212480.jpgMost communication involves content scripts asking for or updating the shared (global extension) state that lives in the background. The remaining requests are usually asking for the background scripts to perform some action(s). In a Chrome extension, there are many contexts that might want to talk to each other (content script, background script, popup window, options page, developer tools script, etc.). Since messaging seemed to be a central piece in all our extensions, we wanted to design a Messaging System that would be as easy to use as possible. With chrome.runtime.Port you can connect extension components together. Each extension component can register a chrome.runtime.onConnect handler that is invoked whenever another component calls chrome.runtime.connect.

But having each extension component talk directly to every other component and maintaining a list of connected counterparts (as these come and go on the fly) could be overly complicated. An obvious simplification of a full-mesh topology is to use a star topology instead, with the background script serving as the central communication hub. The background script is the best choice since it is present over the entire extension lifecycle, whereas the lifetime of the other extension components varies. Also, since the shared extension state lives in background script, it is the background script that benefits most from having an overview of all other extension components.

Our Messaging System calls chrome.runtime.onConnect only once in the background, and each non-background component calls chrome.runtime.connect, usually upon initialization. This creates the communication link between the component and the background. When the component needs something from the background, or vice versa, there is direct channel via which they can talk to each other. In case the component needs something from another component (or components, since we also support broadcasting), it simply sends the request to the background which in turn dispatches it to all the recipients, waits for their responses and sends all of them at once to the original requestor.

JavaScript is asynchronous by nature, so a context may be gone before the script running in it is able to respond. For example, imagine that while waiting for an AJAX response from a remote server in a content script, the user closes the tab where the content script is running. The background script must be aware of this change and react accordingly. In this case, the Chrome API offers the chrome.runtime.onDisconnect handler, which is invoked each time the Port is closing (a reliable way in Chrome to tell that the context is being destroyed). To make broadcasting easier, we decided to give each context group a name. It is an arbitrary string that later allows the user to talk to all instances of the same context type at the same time. This name is provided when the messaging is initiated. For instance, if all content scripts have names starting with content, then one can broadcast messages to all content scripts at once using the name content.

In order to process a message that is passed to a given context, there must be an appropriate handler. In many cases, there must also be a way to pass the result from the handler back to the requestor. (Remember that JavaScript is asynchronous, so we must be able to return the results asynchronously.) So when initializing messaging, we pass in an object holding the message handlers. Keys of the object are command names, corresponding values are functions for processing the given message types (which we call “commands”). Each handler can take any number of arguments. The last argument must be a callback function that is called only once in the message handler and is used for returning the response (i.e. an asynchronous version of the return statement).

API

Let’s look at what the resulting API looks like. Please note that we use the same CommonJS module syntax as Node.js. (We use Browserify to convert the code to something that can be loaded in the browser.) To start using the Messaging System, we must load the appropriate module and initialize it:

// background code:
  var msg = require('./msg').init('bg', { ... });
  msg.bcast(['ct'], 'getURL', function(urls) { ... });

  // content script:
  var msg = require('./msg').init('ct', { ... });
  msg.bg('getUser', function(user) { ... });

The init method takes a mandatory context name (e.g. bg for background script, dt for developer tools or ct for content script), and optionally a handlers object (function lookup table) as described above. A handlers object (with only one handler) might look like this:

 var handlers = { getURL: function(done) { done(document.location.href); } };

The above init function returns a messaging object with three functions:

  • bcast for broadcasting,
  • cmd for single-target commands, and
  • bg for sending messages to background (this function is available in non-background contexts only).

The arguments passed to all three functions are similar. Let’s start with the signature of bcast:

  • tabId: optional integer; if omitted, message is broadcast to all browser tabs
  • contexts: optional string array; if omitted, message is broadcast to all contexts
  • commandName: mandatory string representing command name (key of handlers object),
  • commandArg1, commandArg2, ...: as needed, depends on command handler signature,
  • callback: optional callback function(results) { ... }; if provided it is invoked once when all results are collected

The signature of the cmd method is the same. The only difference is that bcast returns all responses at once, whereas cmd returns only the first response. This is useful when you know that you’ll get only single response. Finally, the bg method doesn’t take the first two arguments, and it behaves as if contexts is set to ['bg'] (i.e. that the target is the background script only). This is the most commonly used method in non-background extension contexts for requesting shared state from or updating it in the background. This Messaging System makes writing Chrome extensions much simpler, but its implementation was complex since we had to take into account so many different scenarios. So we wrote Mocha unit tests for it that test every possible messaging scenario. If you want more details, see our Prezi or check out the code on GitHub.

Roman Kaspar

Roman Kaspar