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 anXmlHttpRequest
- 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 theaction
, as this is what the Worker will use to determine how to respond.
- What I settled on here was this format:
- Populate my listing of Popular Tags, and provide a
title
attribute on eachLI
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#####
- Instantiate the worker - i.e.
var tagListWorker = new Worker('tagWorker.js')
- 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.
- Wire up event handler to for
worker.onerror
so that we can log out errors, etc... - 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 atitle
attribute that tells how many posts have used the tag.
Tag Worker#####
- 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
- function for parsing the XML string (yes, as a string...
onmessage
event handler
- Check the
action
property ofe.data
(theevent
automatically has adata
property that contains thestring
orobject
passed in from the app when it callsworker.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:
- Just used a simple
//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