Table of Contents
Introduction
What is this article all about? Well, it is about a couple of different things that I have never done before, namely writing my own jQuery plug-in and working with HTML5 WebWorker
s.
For those that have never heard of jQuery, it's essentially a JavaScript library. Here is what the jQuery web site says about jQuery:
"jQuery is a fast and concise JavaScript library that simplifies HTML document traversing, event handling, animating, and AJAX interactions for rapid web development. jQuery is designed to change the way that you write JavaScript."
-- http://jQuery.com/ up on date 13/07/2011
Creating your own jQuery plug-in is a well documented thing, which you can read about on the jQuery site, but I will be going through this in some detail in the sections below.
HTML5 WebWorker
s are essentially threads that are created via JavaScript; if you are a .NET kind of chap, think System.Threading.Tasks.Task
. They simple carry out a unit of work in a dedicated thread. We will see more on this later.
So how are these two things used together within this article? Clearly, I needed some use case. So here was the scenario that I came up with, which is what this article does.
I wanted to create a jQuery plug-in that can be applied to a single element, where the jQuery plug-in would accept an array of search terms. For each of the search terms, a new HTML5 WebWorker
is spawned that will do an AJAX Flickr search for images that match that search term. When the HTML5 WebWorker
completes, the HTML5 WebWorker
calls back into the jQuery plug-in, at which point an image wall is created.
In a nutshell, that is what this articles code does. We will be going through this in some detail in the sections below.
Authoring a jQuery Plug-in
As I stated in the introduction, I wanted to create my own jQuery plug-in. As I also stated, this is a well documented process which is available at the jQuery website link: http://docs.jquery.com/Plugins/Authoring.
My jQuery plug-in does not implement all the suggestions at the jQuery website. Here is a list of the features it does implement:
- Context
- Maintains Chainability
- Defaults and Options
To see what these mean, please consult the jQuery website link: http://docs.jquery.com/Plugins/Authoring.
Web Workers: The Basic Idea
HTML5 WebWorker
s are reasonably new and I guess they are not yet in widespread usage. So what is the basic idea behind them? Well, speaking plainly, a WebWorker
is a unit of work that will be carried out on a new thread. Yes, that's right, the ability to create new threads in the browser.
Communication Between Host and WebWorker
Communication between the hosting code (the code that creates the WebWorker
) and the actual WebWorker
is achieved using a PostMessage
like API and a couple of event handlers that the WebWorker
exposes.
Here is what some typical code may look like within the hosting code, notice how we create a new WebWorker
and hook up its two events:
onMessage
, is hooked to the workerResultReceiver
functiononError
, is hooked to the workerErrorReceiver
function
And the worker is started using the PostMessage
API, which is how all message to/from the worker are done.
var worker = new Worker("scripts/FlickrWorkerSearch.js");
worker.onmessage = workerResultReceiver;
worker.onerror = workerErrorReceiver;
worker.postMessage({ 'cmd': 'start', 'msg': settings.searchWords[i] });
function workerResultReceiver(e) {
var result = e.Data;
}
function workerErrorReceiver(e) {
console.log("there was a problem with the WebWorker within " + e);
}
And this is what some typical WebWorker
would look like:
self.addEventListener('message', function (e) {
var data = e.data;
switch (data.cmd) {
case 'start':
postMessage("The worker says hello");
break;
case 'stop':
self.close();
break;
default:
self.postMessage('Unknown command: ' + data.msg);
};
}, false);
This simple example simply sends a string back to the host saying "The worker says hello".
Unit of Work
A unit of work as far as a WebWorker
is concerned is a single JavaScript file that is passed to the WebWorker
when it is constructed, something like this:
var worker = new Worker("scripts/FlickrWorkerSearch.js");
We will see what a typical worker JavaScript file looks like later within this article.
Importing Other Scripts
You can import other JavaScript files in your main WebWorker
by using a line similar to the one shown below:
ImportScripts("scripts/SomeJavaScriptFile.js");
Beware though, the JavaScript files you import must not touch the DOM in any way (so that means they probably can't use jQuery internally either, as jQuery uses window (which is in the DOM)).
Areas of Caution
This section outlines some areas where you must be careful when working with WebWorker
s.
No DOM Access
WebWorker
s can not see the DOM, and as such will not be able to import any script that accesses the DOM, such as jQuery. Which is a shame as jQuery has extra non-DOM related stuff in it that would be nice to use in a WebWorker
, but rules are rules I suppose.
I initially thought I could circumvent this rule by using the DOM in the WebWorker
host and passing in some object into the WebWorker
via a message; alas, this approach failed too, as the object could not be cloned.
The general rule of thumb is that the messages to/from a WebWorker
must be primitive types such as array/string and other simple types.
No Thread Locking Primitives
OK, so we have the ability to run threads, but unfortunately, we have no threading objects to ensure the communication of these threads, and ensure that there is thread safe access to shared data structures. Seems quite a big oversight. What it does mean is that your WebWorker
and its related message handler within the host must pretty much be self contained and not access any shared data structures. I fell into this trap with the code within this article where I was populating a common array, until I realised that this was probably not that smart, and I then moved the array to be scoped locally to the WebWorker
, and then it was all OK.
How the Demo App Works
These subsections shall outline how the demo app works.
How to Run the Demo
The demo is a VS2010 solution for convince, but do not worry if you do not have VS2010, just open Windows Explorer and locate the file "Index.html" and right click and open with Firefox; yes, that's right, I need you to use a specific browser (more on this later).
Browser Support
WebWorker
(s) are supported by the following browsers:
Browser | WebWorker supporting option |
---|
Chrome | v3 or above |
Firefox | v3.5 or above |
IE | v9 or above |
Opera | 10.6 or above |
Safari | 4 or above |
As can be seen from the table above, there is quite good support for WebWorker
(s). Although for this article I am not really concerned with cross browser compatibility and have not spent any time worrying about that at all, yes, you can moan about that if you like, but it will not change anything.
My reasons for this are as follows:
- I wanted to write about
WebWorker
(s), not how to get things to work across browsers - I simply do not have enough hours free to care about this
- I feel this article demonstrates the key concepts behind using
WebWorker
(s) just fine, which is what I was going for
As such, the only browser that I know works for sure is the one that I use most often on the rare occasions I do try and do some web coding, which is Firefox (v3.5 or above). The reason that some of the other browsers do not work is that the free Image Gallery jQuery plug-in that I used does not like different size images coming back to it. Firefox seems to cope with this just fine, but Chrome does not. Go figure. As I say, my focus on this article was the WebWorker
(s) not modifying someone else's Image Gallery jQuery plug-in, so sorry, but it is the way it is.
HTML Part
See JavaScript file: Index.html
Most of the HTML was taken from the freely available Image Gallery jQuery plug-in that this article uses. The only thing that I changed was to include my additional JavaScript files, and to create dynamic content for the DIV class="container"
element via the use of my own custom jQuery plug-in which we discuss next.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>Experimenting With HTML5 WebWorkers</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="description" content="Simple Jquery/Html5
WebWorkers demo to build image wall based on WebWorker Flickr search" />
<meta name="keywords" content="jquery, html5, full screen, webworker, flickr" />
<link rel="stylesheet" href="css/style.css"
type="text/css" media="screen" />
<script type="text/javascript" src="scripts/jquery-1.6.2.min.js"></script>
<script type="text/javascript" src="scripts/jquery.easing.1.3.js"></script>
<script type="text/javascript" src="scripts/FlickrWall.js"></script>
<script type="text/javascript" src="scripts/FullPageImageGallery.js"></script>
</head>
<body>
<div id="webWorkAvailability" style="display :none;" >
<p>WEB WORKERS ARE NOT AVAILABLE</p>
</div>
<div id="fp_gallery" class="fp_gallery">
<img src="images/1.jpg" alt=""
class="fp_preview" style="display: none;" />
<div class="fp_overlay"></div>
<div id="fp_loading" class="fp_loading">
</div>
<div id="fp_next" class="fp_next">
</div>
<div id="fp_prev" class="fp_prev">
</div>
<div id="outer_container">
<div id="thumbScroller">
<div class="container">
</div>
</div>
</div>
<div id="fp_thumbtoggle"
class="fp_thumbtoggle">View Thumbs</div>
</div>
<div>
</div>
</body>
</html>
Basically, this section gets dynamically updated by the use of the custom jQuery plug-in which we discuss next.
<div class="container">
</div>
As I say, most of this HTML was taken from the freely available Image Gallery jQuery plug-in that this article uses.
Custom jQuery Plug-in Part
See JavaScript file: FlickrWall.js
The jQuery plug-in that I wrote does a pretty simple job. It is applied to a specific element, where the jQuery plug-in would accept an array of search terms. For each of the search terms, a new WebWorker
is spawned that will do an AJAX Flickr search for images that match that search term. For each WebWorker
spawned, my custom jQuery plug-in will hook up to both the onMessage()
/onError
events from the newly spawned WebWorker
. When the WebWorker
completes, the WebWorker
calls back into the jQuery plug-in, at which point an image wall is created.
(function ($) {
$.fn.FlickrImageWall = function (options) {
var wwsAreOk = false;
var workersCompleted = 0;
var src = "";
var workerArray = new Array();
var imagesSoFar = 0;
var maxImages = 15;
if (Supports_web_workers()) {
$(".webWorkAvailability").hide();
wwsAreOk = true;
}
var settings = {
'searchWords': ['dog', 'cat', 'shark']
};
function workerResultReceiver(e) {
var workerImages = new Array();
var jsonData = $.parseJSON(e.data);
var src;
for (var i = 0; i < 5; i++) {
src = "http://farm" + jsonData.photos.photo[i].farm +
".static.flickr.com/" + jsonData.photos.photo[i].server +
"/" + jsonData.photos.photo[i].id + "_" +
jsonData.photos.photo[i].secret + "_b.jpg";
workerImages.push(src);
}
PopulateWall(workerImages);
imagesSoFar = imagesSoFar + 1;
if (imagesSoFar == workerArray.length) {
for (var j = 0; j < workerArray.length; j++) {
workerArray[j].postMessage({ 'cmd': 'stop', 'msg': null });
}
}
}
function workerErrorReceiver(e) {
console.log("there was a problem with the WebWorker " + e);
}
return this.each(function () {
if (options) {
$.extend(settings, options);
}
var $this = $(this);
if (wwsAreOk) {
for (i = 0; i < settings.searchWords.length; i++) {
var worker = new Worker("scripts/FlickrWorkerSearch.js");
worker.onmessage = workerResultReceiver;
worker.onerror = workerErrorReceiver;
worker.postMessage({ 'cmd': 'start', 'msg': settings.searchWords[i] });
workerArray.push(worker);
}
}
});
function PopulateWall(images) {
var fullcontent = "";
for (var j = 0; j < 5; j++) {
var imagName = images[j];
var fullstring = "<div class=\"content\"><div><a" +
" href=\"#\"><img src=\"" +
imagName + "\" alt=\"" + imagName +
"\" class=\"thumb\" /></a></div></div>";
fullcontent = fullcontent + fullstring;
}
$(".container").append(fullcontent);
CreateWall();
}
function Supports_web_workers() {
return !!window.Worker;
}
};
})(jQuery);
$(document).ready(function () {
$(".container").FlickrImageWall({ 'searchWords': ['bikini', 'tatoo','water']
});
});
I should point out that the call to CreateWall()
calls into a third party jQuery plug-in which is discussed within this article at Image Gallery Part.
The WebWorker Part
See JavaScript file: FlickrWorkerSearch.js
As you now know, WebWorker
(s) must be created using a standard JavaScript. You now also know that a WebWorker
can not touch the DOM. And I have already stated that my problem domain dictated that each WebWorker
would do an AJAX request against the Flickr API. How is this done? Well, we just use a manual AJAX call.
Here is the full code for the WebWorker
JavaScript file "FlickrWorkerSearch.js" which is used within my custom Query plug-in "FlickrWall.js" when creating the new WebWorker
(s) to do the searching.
function GetData(url) {
try {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, false);
xhr.setRequestHeader("Content-Type",
"application/x-www-form-urlencoded");
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
postMessage(xhr.responseText);
}
}
};
xhr.send(null);
} catch (e) {
postMessage(null);
}
}
self.addEventListener('message', function (e) {
var data = e.data;
switch (data.cmd) {
case 'start':
var url = "http://api.flickr.com/services/rest/?method=flickr.photos.search" +
"&api_key=FLICKR_API_KEY&tags=" + data.msg +
"&safe_search=1&per_page=20&format=json&nojsoncallback=1";
GetData(url);
break;
case 'stop':
self.close();
break;
default:
self.postMessage('Unknown command: ' + data.msg);
};
}, false);
Please Note
I have been good enough to leave in my Flickr API key to give you a fully working demo, but please do not copy it or change the code to slam Flickr with requests, or generally do anything that would cause Flickr to cancel my developer key. Basically, please play nice.
Image Gallery Part
See JavaScript file: FullPageImageGallery.js and jquery.easing.1.3.js
This is perhaps the prettiest part of my demo, and alas I can take no credit for this, this is simply one of many free jQuery image library plug-ins which are available on the internet.
The original source is freely available: http://www.ajaxshake.com/plugin/EN/781/0a2daf19/jQuery-full-page-image-gallery-full-gallery.html where this jQuery plug-in (see "FullPageImageGallery.js") also makes use of a jQuery easing plug-in, which is the "jquery.easing.1.3.js" file.
I did have certain timing issues, due to WebWorker
(s) essentially being a new thread of execution, which the designer of the image gallery obviously did not think about. This led me to re-factor the image library code to allow it to be called on receipt of a WebWorker
posted message inside my own jQuery plug-in. These changes were small and for the large part are not relevant to this article. I did however have to come up with a way to dynamically manipulate the DOM for the window element that was the target for the image library jQuery plug-in. That is not really that relevant to the scope of this article though, rest assured I had to do some DOM manipulation in order to get a dynamic wall using this jQuery plug-in.
I have to say though that the reason I picked this particular image library jQuery plug-in is that it is really, really cool.
But Wait Browser Weirdness
There is however one issue with this free jQuery plug-in, in that it kind of assumes all the images will be of a certain size and that they shall be square (which is obviously something I can not garmented as the images are coming from Flickr). Firefox seems to cope with this just fine but other browsers (such as Chrome) do not. As the main focus on this article was the WebWorker
(s) not modifying someone else's Image Gallery jQuery plug-in, sorry but it is the way it is.
Anyway, apart from the browser issues, once it is populated with images that come back from the WebWorker
(s) AJAX requests, it will use the dynamically created content as shown below, where the user may scroll left to right using the mouse, and then click on an image (click the image below for a bigger image).
Once an image is clicked on, it will expand to full screen at which point the user may scroll through full screen images using the left/right buttons, or return to the thumbnails again. This is shown below (click the image below for a bigger image):
All in all, I was very happy with this free jQuery plug-in.
To be honest, I think Microsoft could learn a lot from the jQuery/community based contributions. Imagine if people could just extend WPF/Silverlight the way you could jQuery and put that stuff out there. I'll give you an example of this image library using an animation jQuery library. So let us look at that some more. I wrote a jQuery plug-in that called a freely available jQuery image library plug-in which in turn depends on yet another freely available jQuery animation plug-in.
If we were in Microsoft land (as I often am), we would have to search the internet searching for Silverlight/WPF controls on CodePlex or wherever and we would probably find something that 10-50 people had downloaded, or we would simply have to wait to see what Microsoft had installed for us with .NET(x).
In contrast, when you search for something like an image library for jQuery, there are literally hundreds to choose from. It is just so much more community based, as it's not so closed to extension, that much is obvious.
However, with anything that open, you will end up with things that look good but end up being untested, rough, and plain unusable; however, if you are willing to take a hunt while on the internet, there are some gems out there.
Me, personally, I like the internet, so I am willing to hunt out the gems.
Conclusion
That's it for now. I know I am not known for my web based community contributions, and to be honest, I do not see that changing, but it is always worth while making sure you know how to do things in popular technologies. I do see that HTML5 will become very popular, but in my opinion, there are some really dumb things in it that need to be fixed such as the things I have found within this article.
You know such as there now being support for extra threading (yes, via WebWorker
(s)), but having no native JavaScript mechanism for doing thread safe stuff is pretty weird, you know things like lock(..)
, and all of the other locking primitives we have available inside .NET/Java or most statically typed languages, heck even most languages for that matter.
I do get the idea of there not being any access allowed to the DOM from the WebWorker
, well, I kind of do. It would have been better to allow it, but it has to be with thread affinity, where some sort of thread affinity object is available to the WebWorker
. Windows has used this model like forever, Java is surprisingly tolerant of thread affinity. I don't care which approach is taken, but the blanket rule of "NO DOM" JavaScript interaction in a WebWorker
is pretty restrictive; you know jQuery has lots of good stuff in it, I know it's primarily a DOM manipulation API, but there is loads of other very useful stuff on offer, such as the ability to do AJAX calls, parse JSON, all sorts of stuff really.
Anyway, enough ranting, hope you enjoyed the article.