Create your own browser extensions, Part 4: Move toward browser-agnostic extensions

Eliminate processor overload and redundancy in an extension for Chrome, Firefox, and Safari

Every browser has its fans, detractors, advantages, and disadvantages. One thing they all have in common is that people increasingly spend more time using them. This series examines how to build the same basic extension for Chrome, Firefox, and Safari. You'll learn what it is like to extend each browser, how hard or easy it is to perform some common tasks, and how to distribute your extension. In this final article of this series, you'll create a common extension that all three browsers can share.

Share:

Duane O'Brien, Software developer, Freelance

Photograph of Duane O'BrienDuane O'Brien is a tired computer scientist. He has written a number of articles on developing web applications and various PHP frameworks. To learn more about Duane, check out his blog or read his tweets.



05 April 2013

Also available in Russian Japanese

Hasn't someone else solved this problem?

Crossrider and the Kango Framework (see Resources) both help you build cross-browser extensions, but each has its limitations. Crossrider's Safari support is less thorough than its support for Firefox or Chrome. The Kango Framework claims to build extensions for all of the major browsers (including Internet Explorer and Opera), but you must pay for a license if your project isn't open source. And both frameworks share a problem: You work with the browsers indirectly, so you're limited to the framework-provided APIs, adding a layer that you have little or no control over. Either framework might be a viable solution for your problem down the line, but you'll benefit from first trying to solve it yourself. Consider it a learning exercise.

So far in this series, you wrote an extension that is called Gawkblocker for Chrome, Firefox, and Safari. With Gawkblocker, you (and its other users) can block certain domains that you prefer not to visit, such as time-consuming blogs. You wrote Gawkblocker for Chrome first, then tweaked it for Firefox, and tweaked it again for Safari. With three similar sets of files to maintain to keep the extension up-to-date for all three browsers, you have extra work and redundant code.

In this final installment in the series, you eliminate redundancy and extra work by coming as close as possible to creating a common Gawkblocker codebase. (See Download to get the complete source code.) You can easily share some things, such as the core JavaScript files and HTML templates. From the previous installments, you know that the storage mechanisms differ from browser to browser, and each browser has a unique API for tracking URL changes. After you complete the article, you can answer the main questions:

  • How much of the extension can be made browser-agnostic?
  • Can it be made browser-agnostic in a way that won't seem like more trouble than it's worth?

Before you start

About this series

In this four-part series, build the Gawkblocker extension for three browsers: Chrome, Firefox, and Safari.

  • In Part 1, take your Google Chrome extension from inception to the app store.
  • In Part 2, build an add-on (or extension) for Mozilla Firefox.
  • In Part 3, tailor the extension to the Safari browser.
  • In this part, tweak your code for browser-agnostic extensions.

Before you tackle this one, complete the first three parts of the series. This article was written against Chrome 23 (Canary build), Firefox 16 (beta channel), and Safari 6.0 (see Resources). Use a Mac if you want to work in Safari, which is unavailable for non-Apple operating systems. You also want a tool that edits HTML, CSS, and JavaScript. If you insist on jumping into this article without completing the preceding parts, do review the setup that is outlined for building extensions for each browser from those articles.

Your reference documents are the Chrome Extension docs, the Firefox Add-on SDK, and the Safari Extensions Reference document (see Resources). As you are condensing what you already know, you are unlikely to refer to them more than a couple of times. You might want to open them, just in case.


Anatomy of Gawkblocker

In Chrome, Gawkblocker uses:

  • A background page to manage the application
  • A JavaScript file that contains most of the logic
  • A pop-up page to show the user what's being blocked
  • An options page for managing blocked sites
  • A landing page to which users are redirected

. Figure 1 shows the pop-up and options pages:

Figure 1. Chrome pop-up and options pages
Screen capture of the Chrome pop-up and options pages

In Firefox, you combined the options and pop-up pages into one page, modified the JavaScript file to use the Firefox storage APIs, and managed the application through a main.js file. Redirected users are sent straight to YouTube. Figure 2 shows the combined pop-up and option page in the Firefox version:

Figure 2. Firefox combined pop-up and options page
Screen capture of the Firefox combined pop-up and options page

In Safari, you used the combined options and pop-up page, a background page to manage the application, and the same JavaScript file you used in Chrome. Figure 3 shows the combined pop-up and options page in Safari:

Figure 3. Safari combined pop-up and options page
Screen capture of the Safari combined pop-up and options page

Moving forward, you'll use a combined options and pop-up page. You'll find out soon enough whether the main.js and background pages must continue to differ per version.


Abstracting browser-specific code

You can start abstracting browser-specific code in a couple of easy places: the pop-up code and the core JavaScript file. Start with the pop-up code.

Figure 4 shows the pop-up/options pages for the Firefox and Safari extensions, displayed side by side:

Figure 4. Firefox versus Safari pop-up page coding
Screen capture of the code for the Firefox and Safari pop-up pages, displayed side by side

The main important differences between the two are how they access the GB object from the background page. In Safari (and in Chrome, not shown) the pop-up code can call to the background page directly. In Firefox, the pop-up code must send a message to the main.js file. In addition, each browser needs a different bit of initialization to run when the pop-up page opens. By externalizing the initializations and the callouts to the GB object, you can use one set of pop-up code.

Create an object that is called BA to handle browser-specific actions and put the object in BA.js. In the object, map each browser to what you must do in the browser. Also, pull the StorageManager object (SM) into its own file. The JavaScript code for the pop-up page looks something like Listing 1:

Listing 1. JavaScript code for the pop-up page
<script src="BA.js"></script>
<script src="SM.js"></script>
<script src="GB.js"></script>
<script>
    $(document).ready(function () {
        BA.handle.popup();
        ...
        showBlockList(GB.getBlockedSites());
    });
</script>

In the BA object, define a map of browsers and the browser-specific things they do. You know with certainty that the BA object must set up the pop-up page, set up the background page, and intercept browser requests, as in Listing 2:

Listing 2. BA object code
actionMap = {
    'safari' : {
        'popup' : function () {
        },
        'background' : function (callback) {
        },
        'intercept' : function (candidate, site, watchthis) {
        }
    },
    'chrome' : {
    ...
}

You also want the BA object to figure out which browser it's in. Test for the availability of APIs — the safari API for Safari, the chrome API for Chrome, and the require and addon APIs for Firefox (depending on whether it's in the main.js or in the pop-up code). Test when you create the module so it is available when you're ready to use it. The code in Listing 3 tests for the availability of the APIs:

Listing 3. Testing availability of Safari, Chrome, and Firefox APIs
    if (typeof safari !== 'undefined') {
        console.log("Safari");
        // Reach the global page using safari.extension.globalPage.contentWindow
        my.handle = actionMap.safari;
    } else if (typeof chrome !== 'undefined') {
        console.log("Chrome");
        // Reach the global page using chrome.extension.getBackgroundPage()
        my.handle = actionMap.chrome;
    } else if (typeof require !== 'undefined') {
        console.log("Firefox - background");
        // Listen for a message using addon.port.on(messagename, callback);
        // Send a message using addon.port.emit(messagename, message); 
        my.handle = actionMap.firefox;
    } else if (typeof addon !== 'undefined') {
        console.log("Firefox - popup");
        // Listen for a message using addon.port.on(messagename, callback);
        // Send a message using addon.port.emit(messagename, message); 
        my.handle = actionMap.firefox;
    }

When you call BA.handle.popup() in Safari, the BA.handle property is already set to the object in actionMap.safari, which contains the popup() setup function you want for that browser.

You do something similar in the background.html pages and the main.js file that you use for the extensions. You call to BA.handle.background() to set up your background page (attach listeners, and so on), and you pass it a function that determines whether to block a site. The JavaScript background.html pages might resemble Listing 4:

Listing 4. JavaScript background.html page
<script src="BA.js"></script>
<script src="SM.js"></script>
<script src="GB.js"></script>
<script>
    $(document).ready(function () {
        ...
        function shouldIBlockThis(candidate) {
            var site,
                blockedSites = GB.getBlockedSites();
            for (site in blockedSites) {
                if (blockedSites.hasOwnProperty(site)) {
                    BA.handle.intercept(candidate, site, GB.getWatchThisInstead());
                }
            }
        }
        BA.handle.background(shouldIBlockThis);
    });
</script>

The code from main.js looks the same; it just uses require to pull in the BA and GB modules. The GB module then imports the SM module, so you can skip requiring it in main.js.


Tweaking the existing modules

Tweak the BA, SM, and GB modules slightly so they are browser-agnostic for the extensions. Specifically, Firefox wants you to export and require modules. Check for exports and require, and use them conditionally. In the GB module, you start with the code in Listing 5 to import the SM module:

Listing 5. Importing the SM module
if (typeof require !== 'undefined') {
    var SM = require("SM").SM;
}

//And you end with this, to export the GB module

if (typeof exports !== 'undefined') {
    exports.GB = GB;
}

That SM module is an issue. Recall that Firefox lacked access to localStorage from within the extension, so in Part 2 you used simple-storage. You can add a handler for that to the BA module, which makes sense. You also can add a check within the SM object. Thus, you can support localStorage in a newer version of Firefox if it becomes available, and keep support for older versions of Firefox. Listing 6 shows this workaround:

Listing 6. Firefox workaround
    if (typeof localStorage !== 'undefined') {
        // Currently supported in Chrome and Safari

        my.get = function (key) {
            return localStorage.getItem(key);
        };
        my.put = function (key, value) {
            return localStorage.setItem(key, value);
        };
        my.remove = function (key) {
            return localStorage.removeItem(key);
        };
    } else if (typeof require !== 'undefined') {
        // This is Firefox.  You call require and use simple-storage
        SS = require("simple-storage");
        console.log("SimpleStorage");
    
        my.get = function (key) {
            return SS.storage[key];
        };
        ...
    }

The workaround also gives you the option to reuse the SM module for another project. With the structure in place, you can plug in the browser-specific code.


Getting browser-specific

Each browser pop-up page is a little different. For Safari, you want to know when the page opens so you can reset the display, as in Listing 7:

Listing 7. Safari pop-up page
'popup' : function () {
    // Safari pop-up pages retain state. Always swap the options and list divs  
to default when the pop-up page closes.
    safari.application.addEventListener("popover", function () {
        $("#onestep").show();
        $("#options").hide();
    }, true);
}

Chrome requires no additional work — nice!

For Firefox, you must set up the port messages and reset the display on open, as in Listing 8:

Listing 8. Firefox pop-up page
'popup' : function () {
    // Firefox pop-up pages retain state. Always swap the options and list divs  
to default when the pop-up page closes.
    addon.port.on("popshow", function () {
        $("#onestep").show();
        $("#options").hide();
    });
    // Firefox pop-up pages cannot get the GB object directly, so you pass messages.
    addon.port.emit("pop");
    $("#watchthis").click(function () {
        addon.port.emit("watchthis");
    });
    $("#makethathappen").click(function () {
        addon.port.emit("makethathappen", $("#watchthatinstead").val());
    });
    $("#blockthistoo").click(function () {
        addon.port.emit("dontgothere", $("#dontgothere").val());
    });
    addon.port.on("blocklist", function (blocklist) {
        showBlockList(blocklist);
    });
    addon.port.on("watchthatinstead", function (instead) {
        $("#watchthatinstead").val(instead);
    });
}

For the background pages and main.js you do something similar. In Safari, add the beforeNavigate event listener, as in Listing 9:

Listing 9. Adding the beforeNavigate event listener in Safari
'background' : function (callback) {
    safari.application.addEventListener("beforeNavigate", callback, true);
}

For Chrome, add the tabs listeners, as in Listing 10:

Listing 10. Adding tab listeners for Chrome
'background' : function (callback) {
    chrome.tabs.onUpdated.addListener(function (tabId, changedInfo, tab) {
        callback(tab);
    });
    chrome.tabs.onCreated.addListener(function (tab) {
        callback(tab);
    });
}

For Firefox, create the panel and widget, and set up the other half of the port communications. Pull in the appropriate modules, as in Listing 11:

Listing 11. Creating the panel and widget for Firefox
'background' : function (callback) {
    var data = require("self").data,
        tabs = require("tabs"),

        GB = require("GB").GB,
        popupPanel = require("panel").Panel({
            height: 500,
            contentURL: data.url("popup.html"),
            onShow : function () {
                this.port.emit("popshow", true);
            }
        });
    tabs.on("ready", function (tab) {
        callback(tab);
    });
    require("widget").Widget({
        id: "GBBrowserAction",
        label: "Gawkblocker",
        contentURL: data.url("GB-19.png"),
        panel: popupPanel
    });
    popupPanel.port.on("pop", function () {
        popupPanel.port.emit("blocklist", GB.getBlockedSites());
        popupPanel.port.emit("watchthatinstead", GB.getWatchThisInstead());
    });
    ...
}

Then you have that intercept handler — the code you start to see whether to block a site. Each handler works slightly differently, but they are structured the same: A tab or event object passes to the intercept handler, along with the site that you're checking against and the target landing page for the user when a site is blocked. The intercept handler takes the necessary action for that browser. For Safari, it looks like Listing 12:

Listing 12. The intercept handler in Safari
'intercept' : function (candidate, site, watchthis) {
    if (candidate.url && candidate.url.match(site)) {
        candidate.preventDefault();
        candidate.target.url = watchthis;
    }
}

For Safari and Chrome, you're done! You can drop in your common files, and they work for both.


A catch

For Firefox, you're not quite finished. Chrome and Safari work easily in this case because both the pop-up and background pages can access the same space within localStorage. In Firefox, you cannot get to the simple-storage object or to objects from the main.js context from within the pop-up code. That's why you do all the message passing. When you generate the list of blocked sites in the pop-up page, in Firefox each site requires an extra click handler to pass a message to main.js. You can add that extra handler inside the block where you create those click handlers, as in Listing 13:

Listing 13. Adding a click handler in Firefox
$("#unblock-" + i).click(function () {
    GB.removeBlockedSite(index);
    BA.handle.removeSite(index);
    showBlockList(GB.getBlockedSites());
});

Then add the removeSite method to the actionMap.firefox object in the BA module, as in Listing 14:

Listing 14. Adding the removeSite method
'removeSite' : function (index) {
    addon.port.emit("unblock", index);
}

If that raises a warning flag, look for better ways. Depending on the needs of your extension, you might find that your code is more exception than not. In that case, step back and reevaluate what you do.

To make things worse, you wind up in an awkward position for Firefox. The pop-up page includes the BA, SM, and GB modules. But all the actual data that you use comes from the main.js file, which also needs the BA, SM, and GB modules. And because they cannot share them, you end up with two copies of each for the Firefox extension, as in Figure 5:

Figure 5. Two sets of modules — not good
Screen capture in Add-on Builder showing the duplicated BA, SM, and GB modules

Duplicated modules are not the best option. You might share the modules or pass them back and forth, but you might think the effort goes too far. This conundrum makes you wonder whether all the effort is worthwhile.


Conclusion

Now you can answer the questions from the start of this article:

  • How much of the extension can be made browser-agnostic? As it turns out, quite a lot. The core JavaScript class and the pop-up code are browser-agnostic. The background page JavaScript is almost identical to main.js, and the other modules work in any of the browsers.
  • Can it be made browser-agnostic in a way that doesn't seem like more trouble than it's worth? Maybe. The GB module is straightforward. The BA module gives you one place to do most of the browser work. You still had to deal with the differences between a Firefox extension and a Chrome or Safari one.

Getting the Firefox extension pop-up code and main.js to share objects, or at least to access the same storage area, has much appeal. You can eliminate almost all the passing of messages if you do that. Don't get too hung up on that one problem. You might get parity among all three browsers if you use something like RequireJS or another CommonJS style loader. And you might replace the BA module with three modules, one for each browser to keep your work more discrete.


Download

DescriptionNameSize
Gawkblocker source codesourcecode.zip6KB

Resources

Learn

Get products and technologies

  • Mozilla Firefox: Download Firefox for your platform.
  • Firefox Add-on SDK: Download the SDK.
  • Firefox Add-on Builder: Access the Add-on Builder here.
  • Gawkblocker: Download Gawkblocker for Firefox from the author's Add-on Builder profile.
  • Chrome Developer Tools: Use the Google Chrome release from the Developer Channel to get the latest version of Developer Tools.
  • Kango Framework: Look at this framework for creating JavaScript extensions.
  • Crossrider: Build cross-browser extensions with this framework.
  • Evaluate IBM products in the way that suits you best: Download a product trial, try a product online, use a product in a cloud environment, or spend a few hours in the SOA Sandbox learning how to implement Service Oriented Architecture efficiently.

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Open source on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Open source, Web development
ArticleID=863536
ArticleTitle=Create your own browser extensions, Part 4: Move toward browser-agnostic extensions
publish-date=04052013