Contents


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

Comments

Content series:

This content is part # of # in the series: Create your own browser extensions, Part 4

Stay tuned for additional content in this series.

This content is part of the series:Create your own browser extensions, Part 4

Stay tuned for additional content in this series.

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

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 Related topics). 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 Related topics). 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
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
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
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
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
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.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

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