Sending Messages to a Content Script in Google Chrome

Google Chrome is coming along very quickly, one of the highly anticipated features due to be released in the near future is the ability to add extensions, something users of Firefox have enjoyed for years. As the documentation says, the API is nowhere near final nor complete, development is very active, things are changing rapidly, and the documentation is out of date.

As a little side project, I am working on a Chrome extension to add web clippings to my Ubernote account, something we have an extension for in Firefox that I helped write the initial clip code for a year and a half ago. Since writing it originally as a bookmarklet, and then having it used in the Firefox toolbar, it has easily become one of the tools I depend on the most. I could use the bookmarklet in Chrome as well, but what's the fun in that, why not try something new?

For those that don't already develop Chrome extensions, there are three possible "parts" to an extension, the toolstrip, a content-script, and a background-page. The toolstrip is what you see, it is the public view to the world, it contains HTML and javascript. There is one toolstrip for every window/tab that is open in Chrome. A content-script is a page with corresponding javascript that is run in the context of the page you are viewing. Like toolstrips, there is one content-script for each window/tab opened in Chrome. Finally, there is a background-page, the background-page is where the Google Chrome Extension API design docs say is where heavy duty processing should occur. There is only one background-page per instance of Chrome, and it exists for the running life of the browser. Because there is one long-running instance, it can be used to store state.

The concept for the project is: I want to hit a button in a toolstrip that clips the contents of a web page and saves it as a note. One of the initial ideas I tried was to basically use the bookmarklet code to attach the meat and potatoes screen scrubber to the current web page, the screen scrubber automatically does the rest of the dirty work just as it has done for the last year and some. While I have since abandoned this method, I did figure out a way to do something that isn't "quite ready" yet in the Chrome extension API.

Content-scripts are made to interact with web pages, so it seemed like a natural fit to put the code to attach the screen scrubber here. With the buttons living in the domain of the toolstrip, I needed a way to communicate to the content-script that the "Clip" button was pressed and it should do some work. In the API documentation there is a section showing how to send a message from the toolstrip to the current tab's content script. Their documentation shows:

chrome/toolstrip.html:
<html>
    <body>
        <button onclick="autolink()" id="autolink">Auto-Link</button>
        <script>
        function autolink() {
            extension.currentTab.postMessage("autolink");
        }
        </script>
    </body>
</html>

I tried something similar to this when I hit my button, sending a message to the currentTab, and bam, exception. In the Chromium Discuss Google group, there was a post written about somebody having the same problem, so I described my basic approach there and asked if there was anything I could do. Aaron Boodman, one of the developers was quick to respond that my approach wouldn't work for now but it was a feature they wanted to add. He also said that initiating communication between a content-script and a toolstrip was a one way street, it had to be initiated by the content-script. Hrmph.

The second approach was to do polling every 500 milliseconds, but that just smelled of being a bad approach and too much work. And then I realized it was so much simpler than I was making it. In the end, I just had to do exactly as Aaron said, have the content script open a port to the extension, and when the button in the toolstrip is sent, send a message back through the already open port. DOH.. Very simple. In the Chrome Extensions Google group, Aaron even posted a code snippet that shows how to send a message from the toolbar to the current tab, assuming a port is open. Here is the code, thanks to Aaron.

In the toolstrip:

<html>
<head>
<script>
// Maps tab id to port.
var portsByTabId = {};

chrome.self.onConnect.addListener(function(port) {
  console.log("got port: " + port.tab.id);
  portsByTabId[port.tab.id] = port;
});

// TODO(aa): Remove ports from the list when ports support an onclose event.

function sendMessageToTab(tabId, message) {
  var port = portsByTabId[tabId];
  if (!port) throw new Error("Could not find a port for tab id: " + tabId);
  port.postMessage(message);
}

function sendMessageToOpenTab(message) {
  chrome.tabs.getSelected(null, function(tab) {
    sendMessageToTab( tab.id, message );
  });
}

function onDoAction()
{
    sendMessageToOpenTab( { message: 'doaction' } );
}

</script>
</head>
<body>
      <div onclick="onDoAction()">
            <span>Do Action</span>
      </div>
</body>
</html>

In the content-script:

var myPort = chrome.extension.connect();

myPort.onMessage.addListener(function(data) {
    console.log( 'got a message' );
    if( data.message == 'doaction' )
    {
        console.log( 'action' );
    } // end if
});

Really not too hard.