O'Reilly logo

JavaScript Cookbook by Shelley Powers

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

21.5. Creating Efficient Desktop Applications with Web Workers and the File API

Problem

You’re interested in adding the necessary functionality so that your browser-based online application can function as a full-featured desktop application.

Solution

In addition to using many of the other technologies covered earlier in the book, add four new capabilities to your JavaScript/CSS/HTML-based application: application cache, so your site can function offline; geolocation, if your application is mobile-based; direct file access through the File API; and Web Workers, for efficient concurrent processing.

Application cache isn’t JavaScript-specific, and the geolocation API requires specialized equipment for testing, so I’m going to focus on the File API and Web Workers in this recipe.

Discussion

The File API bolts on to the existing input element file type, used for file uploading. In addition to the capability of uploading the file to the server via a form upload, you can now access the file directly in JavaScript, and either work with it locally or upload the file using the XMLHttpRequest object.

There are three objects in the File API:

FileList

A list of files to upload via input type="file".

File

Information about a specific file.

FileReader

Object to asynchronously upload the file for client-side access.

Each of the objects has associated properties and events, including being able to track the progress of a file upload (and provide a custom progress bar), as well as signaling when the upload is finished. The File object can provide information about the file, including file size and MIME type. The FileList object provides a list of File objects, because more than one file can be specified if the input element has the multiple attribute set. The FileReader is the object that does the actual file upload.

Example 21-1 is an application that uses all three objects in order to upload a file as text, and embed the text into the web page. In the example, I’m using it to access uncompressed ePub book chapters. Since ePub chapter files are valid XHTML, I can use the built-in XML Parser object, DOMParser, to process the file.

Example 21-1. Uploading an ePub chapter into a web page

<!DOCTYPE html>
<head>
<title>ePub Reader</title>
<meta charset="utf-8" />
<style>
#result
{
  width: 500px;
  margin: 30px;
}
</style>
<script>

window.onload=function() {

  var inputElement = document.getElementById("file");
  inputElement.addEventListener("change", handleFiles, false);
}

function handleFiles() {
  var fileList = this.files;
  var reader = new FileReader();
  reader.onload = loadFile;
  reader.readAsText(fileList[0]);
}

function loadFile() {

  // look for the body section of the document
  var parser = new DOMParser();
  var xml = parser.parseFromString(this.result,"text/xml");
  var content = xml.getElementsByTagName("body");

  // if found, extract the body element's innerHTML
  if (content.length > 0) {
     var ct = content[0].innerHTML;
     var title = document.getElementById("bookTitle").value;
     title = "<h2>" + title + "</title>";
     document.getElementById("result").innerHTML = title + ct;
  }
}
</script>
</head>
<body>
<form>
<label for="title">Title:</label>
<input type="text" id="bookTitle" /></br ><br />
<label for="file">File:</label> <input type="file" id="file" /><br />
</form>
<div id="result"></div>
</body>

Figure 21-8 shows the page with a chapter of one of my books loaded.

Using the File API to read a chapter of an ePub book

Figure 21-8. Using the File API to read a chapter of an ePub book

The File API is still a work in progress, and only Firefox 3.6 and up support it. However, it’s an intriguing functionality that’s also necessary for an application to be considered a “desktop” application—if you want to be able to upload and work with files when your application is currently offline. It’s also pretty handy for other uses.

Note

The File API is a W3C effort. You can read the latest draft at http://www.w3.org/TR/FileAPI/. Read Mozilla’s coverage at https://developer.mozilla.org/en/Using_files_from_web_applications.

The last new technology I’m going to cover in this book is Web Workers. Before I get into the new functionality, though, I want to provide a brief introduction to multithreaded development.

In a language such as Java, you can create multiple threads of execution, which can operate concurrently. Computers and operating systems have long had the ability to support multiple threads, switching the necessary resources among the threads as needed, demonstrated in Figure 21-9. Handled correctly, threads can make your application run faster and more efficiently. Multithreaded development also provides the functionality necessary to ensure the threads are synced, so the applications are accurate, too.

Example of concurrent threads, from the Thread Wikipedia entry

Figure 21-9. Example of concurrent threads, from the Thread Wikipedia entry

In the past, a major difference between JavaScript and these multithreaded programming languages is that JavaScript runs within a single thread of execution. Even when a timer fires, the associated event falls into the same queue as other pending events. This single-execution-thread queue is why you can’t absolutely depend on the preciseness of a JavaScript timer. With Web Workers, introduced as one of the W3C WebApps 1.0 specifications, for better or worse, this all changes.

I say “for better or worse” because thread-based development has always been a two-edged sword in most development environments. If they’re not properly managed, multithreaded applications can crash and burn rather spectacularly. Of course, with most of the other multithreaded environments, you also have more control over the creation and destruction of threads. Web Workers provides threaded development, but at a higher, hopefully safer level.

To create a web worker, all you need do is call the Worker object constructor, passing in the URI for a script file to run:

var theWorker = new Worker("background.js");

You can also assign a function to the web worker’s onmessage event handler, and onerror event handler:

theWorker.onmessage = handleMessage;
theWorker.onerror = handleError;

To communicate with the web worker, use the postMessage method, providing any data it needs:

theWorker.postMessage(dataObject);

In the web worker, an onmessage event handler receives this message, and can extract the data from the event object:

onmessage(event) {
   var data = event.data;
   ...
}

If the web worker needs to pass data back, it also calls postMessage. The function to receive the message in the main application is the event handler function assigned to the web worker’s onmessage event handler:

theWorker.onmessage= handleMessage;

The function can extract any data it’s expecting from the event object.

Normally the script you’d run would be a computationally intensive script, with results that aren’t immediately needed. Mozilla’s example for web workers demonstrates a script that computes a Fibonacci sequence. It reminded me of the recursive function I demonstrated in Chapter 6, the one that reversed arrays.

In Example 21-2, I converted the reversed array function into a web worker JavaScript routine. In the JavaScript library, an onmessage event handler function accesses the data from the event object—the array to reverse—and passes it to the reversed array function. Once the function finishes, the web worker routine calls postMessage, sending the resulting string back to the main application.

Example 21-2. Using web worker JavaScript to reverse an array and return the resulting string

// web worker thread - reverses array
onmessage = function(event) {

   var reverseArray = function(x,indx,str) {
      return indx == 0 ? str :
reverseArray(x,--indx,(str+= " " + x[indx]));;
   }

   // reverse array
   var str = reverseArray(event.data, event.data.length, "");

   // return resulting string to main application
   postMessage(str);
};

I copied and modified the application in Example 21-1 to Example 21-3. When the application retrieves the uploaded file and extracts the body element, it splits the content into an array based on the space character. The application sends the array through to the reversed array web worker. Once the web worker finishes, the data is retrieved and output to the page.

Example 21-3. The ePub reader in Example 21-1, using a web worker to reverse the content

<!DOCTYPE html>
<head>
<title>ePub Reader</title>
<meta charset="utf-8" />
<style>
#result
{
  width: 500px;
  margin: 30px;
}
</style>
<script>

window.onload=function() {

  var inputElement = document.getElementById("file");
  inputElement.addEventListener("change", handleFiles, false);
}

function handleFiles() {
  var fileList = this.files;
  var reader = new FileReader();
  reader.onload = loadFile;
  reader.readAsText(fileList[0]);
}

function loadFile() {

  // look for the body section of the document
  var parser = new DOMParser();
  var xml = parser.parseFromString(this.result,"text/xml");
  var content = xml.getElementsByTagName("body");

  // if found, extract the body element's innerHTML
  if (content.length > 0) {
     var ct = content[0].innerHTML;
     var ctarray = ct.split(" ");
     var worker = new Worker("reverse.js");
     worker.onmessage=receiveResult;
     worker.postMessage(ctarray);
  }
}

function receiveResult(event) {
     document.getElementById("result").innerHTML = event.data;
}
</script>
</head>
<body>
<form>
<label for="file">File:</label> <input type="file" id="file" /><br />
</form>
<div id="result"></div>
</body>

As you can see in Figure 21-10, the results are interesting. Not very useful—except they demonstrate that the web worker performs as expected, and quickly, too.

Firefox 3.6 is the only browser that supports both File API and Web Workers at the time this was written. WebKit, and WebKit-based browsers, Safari and Chrome, support Web Workers. To test the result in the WebKit browsers, I converted the main application to work on a statically created array, rather than an uploaded file:

window.onload=function() {

  var ctarray = ["apple","bear","cherries","movie"];
  var worker = new Worker("reverse.js");
  worker.onmessage=receiveResult;
  worker.postMessage(ctarray);
}

function receiveResult(event) {
     document.getElementById("result").innerHTML = event.data;
}

I ran the application in Firefox and then in Safari, and ended up with unexpected results, as shown in Figure 21-11.

The Firefox results are what I would expect: the array entries are reversed, and the entries converted to a string. The result from Safari, though, is unexpected: every character within the string is reversed, including the commas between the array elements.

Reversed array from uploaded file displayed in page

Figure 21-10. Reversed array from uploaded file displayed in page

Running the application again, but this time in the newest WebKit nightly build, the results match the Firefox result. What happened with Safari 4 is that postMessage didn’t serialize the object correctly when it transmitted the object to the web worker routine. The newer WebKit nightly shows that this bug was fixed, and the object is now serialized correctly.

However, to ensure that objects are passed correctly, regardless of browser version, we can use JSON.stringify on any object before we send it, and JSON.parse on any data that we receive, in order to ensure the object is handled correctly regardless of browser and browser version:

window.onload=function() {

  var ctarray = ["apple","bear","cherries","movie"];
  var worker = new Worker("reverse2.js");
  worker.onmessage=receiveResult;
  worker.postMessage(JSON.stringify(ctarray));
}

function receiveResult(event) {
     document.getElementById("result").innerHTML = event.data;
}

In the web worker, we use JSON.parse to restore the serialized version of the object:

// web worker thread - reverses array
onmessage = function(event) {

   var reverseArray = function(x,indx,str) {
      return indx == 0 ? str :
reverseArray(x,--indx,(str+= " " + x[indx]));;
   }

   // reverse array
   var obj = JSON.parse(event.data);
   var str = reverseArray(obj, obj.length, "");

   // return resulting string to main application
   postMessage(str);
};
Interesting—and differing—results when running application in Firefox and Safari

Figure 21-11. Interesting—and differing—results when running application in Firefox and Safari

Now the application works the same in Firefox 3.6 and Safari 4. The application should also work with Chrome. Web workers don’t work with Opera 10.5, or IE8. However, both browsers should support web workers, and hopefully the File API, in an upcoming version.

See Also

See Recipe 6.6 for a description of the array reversal recursive function. The postMessage method is introduced in Recipe 18.10, and JSON is covered in Recipes 19.4 and 19.5.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required