Isolated Worlds

Chrome Extensions have been praised as being really easy to write, especially when compared with writing a Firefox xpi or a plugin for IE. But there’s still a few concepts that trip up developers regularly. One of these is the isolated worlds concept, and it leads to a lot of support questions in the group.

For example:

My requirement is load jquery.js file into the content script
when it is not having that js file(For Ex: I am checking for jquery.js
file,fancybox.js file and if it is not there load these files. )

When i implement this logic in content script it is loading the
jquery.js file. After that it is not working in content script . It is
showing $(or) jQuery is undefined. For every time it is loading,but
not executing in the content script.

The code in question usually winds up being something like:

   function loadJQuery() {
     if (!jQuery) {
       var script = document.createElement('script');
       script.async = true;
       script.src = "http://url/to/jquery.js";
       script.addEventListener('load', function() {
         // Do something with jQuery, which causes an error.
       });
       document.head.appendChild(script);
     }
   }

This is a pretty interesting situation because if you were to use the snippet on a regular web page, there would be a decent chance of getting it to work. But Content Scripts don’t work just like regular web pages, and not understanding that difference will lead to painful-to-debug issues like this one.

An overview of extension architecture

A lot of Chrome extensions have structures similar to this:

'Standard extension format'

That is, a manifest.json file which points to a background page, a popup, and a content script. The content script runs on a few URLs defined in the manifest, the popup is shown whenever a user clicks on a browser or page action (also defined in the manifest) and the background page just runs all the time.

The three files pointed to by the manifest have a lot of similarities. They run JavaScript, have access to a lot of the built-in DOM/JS functions in the browser, have some access to the chrome.* namespace, and can communicate with each other.

The communication is where a few differences pop up. You might expect that all parts of an extension are alike, but that’s not true. For example, a background page and a popup both exist in the same Chrome process, so they can directly share memory. This means that you can access a background page’s window object from a popup by calling chrome.extension.getBackgroundPage and access a popup’s window object from a background page by calling chrome.extension.getViews:

'Background page and popup talking to each other'

This is pretty cool, because a popup could log to the background page’s console like this:

var bg = chrome.extension.getBackgroundPage();
bg.console.log('This is sent from the popup but shows up in the bg page log!');

Content scripts are a different story. They interact with a rendered page, so they need to be loaded in that process. This means that the content script and the background page cannot directly share objects in memory:

'Background page and content script are separated by a process boundary'

The two can still pass messages back and forth using chrome.extension.sendRequest but these messages are serialized and deserialized along the way - you can’t pass function or object references back and forth.

I think that most extension developers handle this distinction pretty well. At the very least, it’s easy to think content scripts are just like the regular page and move on.

But that’s not completely right- there’s one more level of isolation in effect, which is that a content script can’t access everything in the web page it is currently running on. While the content script can read and write the DOM nodes on the page without any issue, it can’t access native JavaScript code in the page:

''

The reason for this has a lot to do with JavaScript’s dynamic nature. If the page and the content script shared a JavaScript execution environment, then the page could conceivably start calling things like chrome.extension.sendRequest and messing around with the internals of the extension. Since extensions have a lot more power over the system than regular websites, this is a no-no.

Fixing the problem

So how does one load jQuery into the content script’s execution environment dynamically? Unfortunately, you can’t. Loading JavaScript requires inserting a node into the DOM, and once the DOM loads a script, the code is executed in the context of the untrusted page.

So this code:

function loadJQuery() {
  var script = document.createElement('script');
  script.src = "http://url/to/jquery.js";
  document.head.appendChild(script);
};

does actually load jQuery, but it puts it into the page’s execution context, not the context of the content script. In fact, if you were to do something like:

function loadMyScript() {
  var script = document.createElement('script');
  script.src = chrome.extension.getURL("myscript.js");
  document.head.appendChild(script);
};

you would be able to load JavaScript resources packaged with your extension into the context of arbitrary web pages. Which can be useful if you really need to get into that JavaScript context for whatever reason.

An extension can still rely on jQuery, although not dynamically. Just reference the appropriate file in the content_scripts section of your manifest.json file:

{
  ...
  "content_scripts": [
    {
      "matches": ["http://www.google.com/*"],
      "js": ["jquery.js", "myscript.js"]
    }
  ],
  ...
}

Of course you knew that, since it’s almost verbatim from the documentation, right?

Comments? In the interest of limiting third-party Javascript running on this site, I've disabled all commenting plugins. If you have feedback, please share it with me on Twitter.

Elsewhere

Twitter (@kurrik) Github (kurrik) Google+ (+kurrik) Linkedin (kurrik)

Tags

arne (10) reviews (9) work (7) chrome (7) twitter (7) extensions (6) games (5) cinemaclub (5) html (4) books (3) go (3) algorithms (3) presentations (3) javascript (3) google (3) ludumdare (2) estonia (2) appengine (2) internet (2) product (1) readinglist (1) art (1) management (1) space (1) recipes (1) http (1)