Krzysztof Kaczor

Passionate Software Developer

Injecting content with chrome extension

on 15. September 2015

Even though Google Chrome has powerful mechanism for building browser extensions, APIs exposed to programmers are often verbose and a little bit cryptic. In this post I want to briefly describe ways of injecting arbitrary content into websites.

During work on one of my side projects I needed to display simple popup informing user about status of the current action. I wanted to display information directly in current website window i.e. inject some JS and CSS rather than use extension popup - small popup displayed after hitting extension button. There are few ways of doing this:

  • content script - you can specify so called content scripts in your manifest.json These are scripts that run in context of a given page. Unfortunately Chrome requires to specify domains that will trigger your content script - you can’t define ‘global’ content script that would run with every single page in the internet. They are handy when you need to inject some code/change some styles into every page from given URL. I used that approach in small plugin for fixing shortcuts in google search.
  • chrome.tabs.executeScript and chrome.tabs.insertCSS these are methods that allows for injecting content into given tab. We can use them fire them for example form background script. We will focus on that method because it’s very flexible.

Loading resource

You can load given JS script by using injectScript function. File path is relative to package root.

chrome.tabs.executeScript(null, {file: 'packages/jquery.min.js'}, function() {
    console.log('Its loaded!');
});

Pretty easy, right? To load dynamically file from arbitrary URL by sending ajax request (but beware of CORS protection in chrome extensions!) and then just load that code as string:

chrome.tabs.executeScript({
    code: 'string with js code'
});

Similarly you can inject CSS file:

chrome.tabs.insertCSS(null, {file: 'packages/styles.css'}, function() {
    console.log('Its loaded!');
});

Highway to (callback) hell

Now just imagine that you need to load few dependencies before executing your code.

chrome.tabs.insertCSS(null, {file: 'toastr.min.css'}, function () {
    chrome.tabs.executeScript(null, {file: 'jquery.min.js'}, function () {
        chrome.tabs.executeScript(null, {file: 'toastr.min.js'}, function () {
            chrome.tabs.executeScript({
                code: "toastr.info('Hello chrome!')"
            });
        });
    });
});

I think that everyone agrees that it’s awful. Let’s think about a better way.

Promises to the rescue

When talking about callbacks we should try as soon as possible change conversation’s subject to promises ;) The good news is that we can leverage ES6 promises - newest chrome versions supports them natively - no transpilers required! Let’s wrap chrome APIs:

/**
 * Injects resources provided as paths into active tab in chrome
 * @param files {string[]}
 * @returns {Promise}
 */
function injectResources(files) {
    var getFileExtension = /(?:\.([^.]+))?$/;

    //helper function that returns appropriate chrome.tabs function to load resource
    var loadFunctionForExtension = (ext) => {
      switch(ext) {
          case 'js' : return chrome.tabs.executeScript;
          case 'css' : return chrome.tabs.insertCSS;
          default: throw new Error('Unsupported resource type')
      }
    };

    return Promise.all(files.map(resource => new Promise((resolve, reject) => {
        var ext = getFileExtension.exec(resource)[1];
        var loadFunction = loadFunctionForExtension(ext);

        loadFunction(null, {file: resource}, () => {
            if (chrome.runtime.lastError) {
                reject(chrome.runtime.lastError);
            } else {
                resolve();
            }
        });
    })));
}

Now rewriting previous example of showing toastr popup:

injectResources(['components/toastr.min.css', 'components/jquery.min.js', 'components/toastr.min.js']).then(() => {
  chrome.tabs.executeScript({
    code: `toastr.info('Hello chrome!')`
  });
}).catch(err => {
  console.error(`Error occurred: ${err}`);
});

As a bonus we get async loading of resources. Pretty neat, huh? :D