2012 0009 0015
I launched nonpartisan.me a few weeks back, which exists
primarily in the form of a Google Chrome extension (there’s a Firefox
add-on too). Since I released it with all of the source, this
makes it a great time to dissect the (very simple) code. As you will
notice from the site and the small bit of press it picked up,
nonpartisan.me
has a very simple premise: filter out political
keywords from the various newsfeeds (specifically Facebook, Twitter,
and Google+).
This was my first attempt at a Chrome extension, and it’s surprisingly
straightforward. All such extensions require a manifest.json
, which
looks like this for nonpartisan.me
:
The real meat here is content_scripts
, which lists the javascript we
wish to trigger after a page is loaded, greasemonkey
-style. A
particularly nice feature of content scripts are that they work in an
isolated environment separate from any javascript that the page itself
may include. Thus we can add jquery
to the list of javascript that
is run without fear of clashing with a page’s global namespace.
You can think of every element in the "js"
array as a separate
<script>
tag in an HTML
page, so the files are loaded in the given
order, all into a single namespace. Rather clumsily, I chose to
simply put a callback module (which is called plugin
here) in the
individual fb.js
, tw.js
, and gp.js
files which is then used by
the core component, nonpartisan.js
, as a simple means of avoiding
any hard-coded per-site values in the actual filtering code.
With this, and the pseudo-regex "matches"
field that specifies which
pages trigger the content script, we can run arbitrary code on
websites we specify. For nonpartisan.me
, the filtering code looks
like this:
The first chunk–the kill
function–works as advertised: given a
parent element and a set of keywords, the function iterates over every
child element and determines if any of the nested elements within
(i.e. el.find('*')
) contains any of the keywords. Instead of
deleting DOM
nodes, which may break the page’s own javascript (I
discovered this the hard way), it’s easier to instead call
el.css({'display':'none});
to simply hide unwanted elements. For
efficiency, the forEach
terminates as soon any any nested child
returns a match, potentially saving a small amount of needless
searching.
The second chunk starts a timer (if indeed the parent is even found on
the current page) that checks if the number of children of the parent
element has changed and, if so, re-triggers the filtering process to
determine if there are any new children to be hidden. This helps
handle AJAX
-driven sites, like the “infinite scrolling” facebook
newsfeed, which may mutate the DOM
at any time. Both of these
functions are wrapped up into another easy-to-call function inside of
the high-level nonpartisan
module.
And that really is all there is to a typical greasemonkey
-like
Chrome extension, but that’s certainly not the end of what a complete
and helpful extension can provide. The trickier bit is persisting
configuration options. The downside of sandboxing content scripts is
that they exist in a transient execution context, meaning there’s no
localStorage
to persist program options. The details of the
plumbing used to kick-off the process and handle options were omitted
from the above snippet, so we’ll dig more into this now to illustrate
how to handle persistent options.
Chrome provides a nice solution to the problem of not having
localStorage
available to content scripts by providing a
background
script which does have its own localStorage
, which it
can transmit to a content script via the chrome.extension.onMessage
listener. We can then fill in the omitted component of the above
snippet with:
This sends a message, requesting "config"
from the background.js
script, which returns, among other things, the list of keywords we
wish to filter. This list was saved in localStorage
in
background.js
’s execution context. Recall that plugin
is the
module that specifies the particular settings for the page being
filtered. Thus we pass along the list of words to filter and the
nonpartisan()
callback function to the plugin
module, and it
subsequently executes nonpartisan()
on the appropriate elements on
the DOM
. The background.js
file used in nonpartisan.me
is a bit
more involved, but it nonetheless essentially acts as a broker,
converting Chrome’s internal message-passing API calls to
localStorage
requests.
Of course, there’s only so much utility to be gained from
localStorage
without supplying the user with the ability to
configure the various options that may be saved in therein. This is
done by a typical html
page, specified by "options_page"
. Since
there’s not much magic there–it’s just a plain html page with enough
javascript to persist the settings–I will omit the gory details,
which you can poke around yourself in the repository, if
you’re so inclined.
So that’s an extension. Writing the above was literally a matter of minutes and some quality time with the Chrome API specifications. As is always the case (especially when I’m working outside of my area of expertise, say with making the amateurish logo), the real work is doing the little bits of spit-and-polish to handle the various configuration options, throwing together the webpage, creating the icons and promotional images for the Chrome Web Store, etc. But it’s still good to know that the Chrome team has made the extension-building process as simple and well documented as they have.