Web Workers pt 1 - A Blog's Tags

I use the Ghost blogging platform, and love most everything about it. However, as with any platform, sometimes it's necessary to do some parts yourself. As a software geek, I don't mind this ;-)

So, what I wanted to do was to dynamically provide a "Popular Tags" listing on my blog. However, there isn't yet a way for me to get some generic listing of all my tags used and the count of times each one is used. I know they are working on some API functionality and this may be doable when that gets implemented, but seeing as my 15 month old, Garrison, decided that 4AM on Valentine's Day is the appropriate time to wake up... I decided not to wait :)


The Plan###

Since this isn't an included feature in the platform (to my knowledge), I had to come up with a plan to implement myself. Fortunately, Ghost handles generating RSS feeds for the blog automagically - so that's where I decided to get all my info. In a nutshell, here's the plan:

  • Have a handlebars template for my standard list of popular tags (see Ghost's themes documentation if curious about this) so there's some default data.
  • Use a Web Worker to asynchronously GET the XML of the RSS Feed by using an XmlHttpRequest
  • Implement a standard set of messaging so that my main app can communicate with the Web Worker and give it commands or respond to its messages.
    • What I settled on here was this format: {action: 'getStuff', someVal: 'yarrrrg', content: 'Hey there!' ...} with the most important part being the action, as this is what the Worker will use to determine how to respond.
  • Populate my listing of Popular Tags, and provide a title attribute on each LI element showing how many posts have used the tag.

The Code###

Typically, I write out a very detailed tutorial-type approach in my posts. However, I thought that this set of functionality is a bit larger than a quick tutorial, and that other bloggers and such may find it useful. So, I'm going to outline the high-level overview of the responsibilities of each component here and then link to the github repo I created for this. Oh, not to mention that you can open dev tools on this very page and see the code. Check out /assets/js/web-workers/tagWorker.js :)

Application#####
  1. Instantiate the worker - i.e. var tagListWorker = new Worker('tagWorker.js')
  2. Wire up event handler so that when the Worker posts a message back to this thread we can examine the data and then react accordingly.
  3. Wire up event handler to for worker.onerror so that we can log out errors, etc...
  4. Once we have the tags we need, add them to the DOM and make sure to keep href functionality in tact so that users can click on the tag and see all posts. Also, want to provide a title attribute that tells how many posts have used the tag.
Tag Worker#####
  1. Upon instantiation:
  • Wire up the onmessage event handler to be able to react to messages sent from the application thread. self.onmessage = function msgReceived(e){...}
  • Create helper functions and variables needed. This includes things like:
    • function for parsing the XML string (yes, as a string... DOMParser not available in Web Worker, so can't use it to parse XML)
    • variable to hold a reference ot the current XMLHttpRequest in process (if any)
    • variable to hold a reference to all callbacks to fire when the xhr is done and the XML is parsed into tags
  1. onmessage event handler
  • Check the action property of e.data (the event automatically has a data property that contains the string or object passed in from the app when it calls worker.postmessage(...)) to determine the appropriate method to fire as a result.
    • Just used a simple switch case statement here, and then was able to either invoke a method directly, or invoke the helper to load the RSS XML and pass in the appropriate method as the callback:
//tagWorker.js - onmessage evt handler
//entry point for all messages coming to this Worker
self.onmessage = function(e){
	var msg = e.data;

    try {
        switch (msg.action) {
            case 'setRSSUrl':
                rssUrl = msg.url;
                break;
            case 'getUniqueTags':
            	//requires tags to be loaded, so pass target fn as callback to loadTags fn
                loadTags(getUniqueTags);
                break;
            case 'getMatchingTags':
            	//requires tags to be loaded, so pass target fn as callback to loadTags fn
                loadTags(function () {
                    getMatchingTags(msg.match);
                });
                break;
            case 'getMostUsedTags':
            	//requires tags to be loaded, so pass target fn as callback to loadTags fn
                loadTags(function () {
                    getTopNTags(msg.count, msg.excludeMatches || []);
                });
                break;
            default:
                throw ["Unknown Action: ", msg.action].join('');
                break;
        }
    } catch (ex) {
        throw ["Unable to handle '", msg.action,  "'.  Reason: ", ex].join('');
    }
};

And, that's it in a nutshell. The beauty here is that nothing is blocking the UI thread! All of the heavy stuff is done by the Worker. It requests the RSS data, waits for the response, tracks its own list of callbacks for when tags have been obtained, and handles parsing the tags out of the string-ified RSS XML. Only after all that work is done do the callbacks get fired, and subsequently the app's event handler to manipulate the DOM. Good stuff!! Please check out the GitHub repo I created as an example of this process for much more detail. Also, don't forget that you can view the Worker script running on this page by opening your dev tools and then navigating to `assets/js/web-workers/tagWorker.js' :)


Happy Valentine's Day!!!!###

--Bradley