In a previous article, I looked at the HTML5 <video> tag and History object, two of the many features that developers have started to implement to give users of their apps new input controls, jQuery Mobile features, location and mapping, and much more. In this article, I continue exploring HTML5 features with a look at Web Workers, which let you speed up your client-side applications, and Indexed DB, a client-side data storage mechanism that is the preferred storage approach going forward in HTML5. (Before Indexed DB—also known as the Indexed Database API—work was focused on a standard named WebSQL. That work was discontinued in the fall of 2010. IndexedDB is the result of follow-on work to create a standard for data storage in Web browsers.)
Note: The APIs for HTML5 vary slightly across different versions of browsers and different implementations. Because HTML5 is still in the recommendation phase at the W3C, you should think of it as a draft at this point. Because these APIs may change before they become a final standard, I cover only the version of HTML5 implemented in Internet Explorer 10. Every opportunity will be made to test the code across other browsers to verify that it works.
Back in the day, Web sites were just containers of HTML content. Making changes to the UI required a trip to the server, and the server would return a new page of content that the browser presented to the user. This was clumsy and not very bandwidth friendly. Then AJAX (Asynchronous JavaScript and XML) showed up, and that enabled scripts to call out asynchronously to services across the Web. AJAX showed how Web applications could make use of asynchronous operations, and asynchronous operations, like threading, improve the user experience by allowing multiple operations to be completed at the same time. Various computer operating systems have supported the use of threads since before I started programming, so this is a fairly well-known feature among developers. Until HTML5 and the Web Workers standard, however, JavaScript has not had the ability to work with background threads, only the asynchronous operations provided by the XMLHttpRequest object that AJAX uses. Web Workers are a great step forward. With the inclusion of support for Web Workers in browsers, Web applications can provide more of the responsive feel that users expect in their applications.
Note: There are two types of Web Workers: Dedicated Workers and Shared Workers. For the purposes of this article, I use Web Workers (or just Workers) to refer to Dedicated Workers.
HTML5 Web Workers is a specification that allows long-running scripts to perform in the background. These scripts run independently of the user interface and any scripts running there. This means that long-running tasks aren’t accidently interrupted. Much like threads in the .NET Framework, Web Workers take a while to create and are fairly heavyweight. As a result, you should not create too many of them. In general, Workers are expected to be used for an extended period of time.
Web Workers are useful for many of the same operations that threads in a desktop application perform. These include:
Let’s start by looking at Web Worker–related objects that developers need to work with, along with the APIs that developers have to use:
Here’s an example of some HTML code that calls a Web Worker in an operation that calculates prime numbers (see Figure 1). When the page loads, a check occurs to ensure that the browser supports Web Workers. If the browser supports them, a Worker is created, events are wired up, and the execution begins with the postMessage method call.
- <p>The highest prime number discovered so far is:
- <output id="result"></output></p>
- <button id="stop" />
- <script>
- var stopped = true;
-
var ww = Modernizr.webworkers;
- var worker;
-
var stop = "Stop Processing";
- var stopBtn;
-
stopBtn = document.getElementById("stop");
- stopBtn.innerHTML = stop;
-
if (ww) {
- worker = new Worker('@Href("~/Scripts/calc.js")');
-
worker.onmessage = function (event) {
- document.getElementById('result').innerHTML = event.data;
- };
- worker.onerror = function (error) {
- document.getElementById("result").innerHTML = error.message;
-
}
- worker.postMessage({"cmd":"start"});
-
stopBtn.addEventListener("click", quitProcessing, false);
- stopBtn.enabled = true;
-
}
- else {
-
document.getElementById('result').innerHTML =
- "This browser does not support Web Workers.";
- }
- function quitProcessing(e) {
- if (worker != null) {
-
worker.terminate();
- }
-
}
- </script>
Note: You may notice a library or object in this code called Modernizr. Modernizr is a JavaScript library that is used to test a browser for the ability to perform certain pieces of functionality. A link to anMSDNMagazine article on Modernizr is available in the references at the end of this article.
Now let’s look at the Web Worker file calc.js. In this file, the code handles the onmessage event. The function checks to be sure that a defined value has been passed in. With a defined value passed in, the process to calculate whether a number is a prime number begins.
- self.addEventListener('message', function (e) {
- var data = e.data;
- switch (data.cmd) {
- case 'start':
- var n = 1;
- search:
-
while (true) {
- try{
- n += 1;
- for (var i = 2; i <= Math.sqrt(n) ; i += 1)
- if (n % i == 0)
- continue search;
- // found a prime!
- postMessage(n);
-
}
-
catch( err ){
- throw err;
- }
- }
- break;
- case 'stop':
- self.close();
-
break;
-
default:
-
self.postMessage("Command posted: " + data.msg);
-
}
-
}, false);
Figure 1. Worker Example
Note: In my code I show different ways of doing things. For example, some events are wired up using an assignment of an event while others have been wired up using the addEventListener syntax available in the DOM. This has been done to show options and not to show favoritism to one mechanism over another.
I’ve shown a simple example of how to implement Web Workers, but there are many other ways that developers can implement threading to improve the user experience. Any algorithm that can be divided into smaller parts is a candidate for using Web Workers.
If you’ve been working with HTML5 (or been reading up on it), you’ve mostly heard of several Web-based client-side technologies related to storage:
Developers working in the database world are familiar with Structured Query Language (SQL) in some form through their work in SQL, LINQ, Entity Framework, Hibernate, or similar approaches. Over the past few years, a new data storage movement, called NoSQL, has been making some headway, and IndexedDB borrows many ideas from the NoSQL movement. (For more information on NoSQL, check the references listed at the end of this article.) Just like in a common NoSQL data store, IndexedDB uses key-value pairs. The key-value pairs are saved in the data store and allow for relatively quick lookups.
One of the first things you should know about IndexedDB is that it has two official sets of APIs that effectively mirror each other: the first set is synchronous, and the other asynchronous. Only the asynchronous APIs are available in Internet Explorer (or any browser). The synchronous APIs are designed for running in Web Workers and in other situations where the code path is already asynchronous. Because developers will work with asynchronous APIs, they need to be aware of when events fire, how to handle these events, and how these events can be chained together.
Creating a database in IndexedDB is fairly simple. The window object now has an IndexedDB object within it, and the IndexedDB object contains the API calls for IndexedDB. (Older versions of browsers might contain versions of the IndexedDB object that are prefixed, and they most likely contain versions of the API that are not up to date.)
Creating and opening a database are performed by the same operation. The database is opened via a call to the .open method, which takes two parameters: a database name and the version number that will be used. Keep in mind that the call to .open is asynchronous. It returns an object, and your code needs to handle the events on the object. The events that the .open method can fire are:
Here’s some code that shows an example of creating and opening a database:
- var request = indexedDB.open("team", 1);
- request.onsuccess = function (evt) {
- db = request.result;
-
};
-
request.onerror = function (evt) {
- console.log("IndexedDB error: " + evt.target);
- };
After a database is open and available, the next step is to add some records to the database. In this example I have the five starting members of my children’s basketball team from a few years ago. The data includes:
Let’s walk through the steps of adding some records:
Here’s the sample code. Notice that I’ve included the onupgradeneeded event.
- var request = indexedDB.open("team", 1);
- request.onsuccess = function (evt) {
- db = request.result;
-
};
-
request.onerror = function (evt) {
- console.log("IndexedDB error: " + evt.target);
- };
- request.onupgradeneeded = function (evt) {
- var players = [{ num: 10, name: "kMac", age: 16, position: "point guard" },
- { num: 12, name: "Brad", age: 15, position: "small forward" },
- { num: 23, name: "Josh", age: 15, position: "shooting guard" },
- { num: 32, name: "Patrick", age: 15, position: "power forward" },
- { num: 42, name: "Elo", age: 16, position: "center" }]
- // Create an objectStore to hold information about our team. We're
- // going to use "num" as our keypath because it's guaranteed to be
- // unique on our team.
- var objectStore = evt.currentTarget
- .result.createObjectStore("players", { keyPath: "num" });
- // Create an index to search players by name. We may have duplicates
- // so we can't use a unique index.
- objectStore.createIndex("name", "name", { unique: false });
- // Create an index to search players by position. We want to ensure that
- // no two players have the same position, so use a unique index.
- objectStore.createIndex("position", "position", { unique: true });
- // Store values in the newly created objectStore.
- for (var i in players) {
- var request = objectStore.add(players[i]);
- request.onsuccess = function (event) {
- rd.innerHTML = "Added.";
- }
- request.onerror = function (event) {
- // somehow respond to the user that data has been added.
- // rd.innerHTML = "Error:" + event.message;
- }
- }
- }
So far we’ve seen how to create a database, create an object store, and add data to the object store. Now let’s take a look at how to query data. Here are the specific steps:
One thing to notice is the use of the .continue method to move to the next record in the cursor. There are also other methods for iterating through the cursor and operating on the records. These include:
Here’s the code that demonstrates our query:
- var dbName = "team";
- var request = indexedDB.open(dbName, 1);
- request.onsuccess = function (e) {
- try{
- var db = e.target.result;
- var transaction = db.transaction(["players"]);
- var objectStore = transaction.objectStore("players");
- var index = objectStore.index("num");
- // Only match
- // var singleKeyRange = IDBKeyRange.only(12);
- // Match anything past 10, including 10
- // var lowerBoundKeyRange = IDBKeyRange.lowerBound(10);
- // Match anything past 10, but don't include 10
- // var lowerBoundOpenKeyRange = IDBKeyRange.lowerBound(10, true);
- // Match anything up to, but not including, 32
- // var upperBoundOpenKeyRange = IDBKeyRange.upperBound(32, true);
- // Match anything between 9 & 32.
- var boundKeyRange = IDBKeyRange.bound(9, 32, true, true);
- var cursor1 = index.openCursor(boundKeyRange);
- var cursor2 = index.openCursor(IDBKeyRange.only(10));
- cursor1.onsuccess = function (event) {
- try{
- var cursor = event.target.result;
- var out = "";
- if (cursor) {
- // Do something with the matches.
- // alert(cursor.value.name);
- cursor.continue();
- }
- }
- catch (err) {
- alert(err.message);
-
}
-
};
-
cursor2.onsuccess = function(event){
- var cursor = event.target.result;
- }
- }
- catch (err) {
- alert(err.message);
-
}
-
}
You can open a transaction using one of several states:
In a SQL-based database, developers often use an “order by” command. An IndexedDB cursor uses the same concept. When you call openCursor in IndexedDB, you can pass in a second parameter. This parameter is a string and can have the following values:
Deleting a record is rather easy. All that’s needed is to call the delete method in the object store while passing in the key of the object that needs to be removed. As with the other methods in IndexedDB, this is an asynchronous call, and code can be called after the event fires.
- var request = db.transaction(["players"],
- "readwrite").objectStore("players").delete(12);
- request.onsuccess = function (event) {
- // player has been removed.
- };
A slightly different calling procedure is used in this example. Here, I’ve removed the chaining of events that was included previously.
If you need to delete a data store, use the deleteDatabase method, as shown here:
- var dbd = window.indexedDB.deleteDatabase("team");
- dbd.onsuccess = function (e) {
- rd.innerHTML = "deleted";
- }
- dbd.onerror = function (e) {
- rd.innerHTML = e;
-
}
Whenever data is stored in a browser, the question of security comes up (or at least it should). Thankfully, IndexedDB is governed by the same-origin policy, which means that features embedded in the browser can access only other features that originated from the same site. For example, an IndexedDB data store created by http://example.com/page1.html can be accessed only by another page from http://example.com.
Along with security concerns, privacy issues arise when you store any type of personally identifiable data on any device (PC, phone, tablet) outside a controlled data store. Take care when storing any type of personally identifiable information in this way. One thought is to use IndexedDB as a short term cache for application data. Remember, if a malicious hacker has physical access to a device, there is a high likelihood that given enough time, the hacker can get access to the data stored on the device.
In this article, I’ve introduced you to the HTML5 IndexedDB and Web Workers. Thanks to the great support for HTML5 features in Internet Explorer 10, Windows 8, Windows RT, and Windows Phone, developers have the opportunity to build some really fantastic apps. I hope that you find this article helpful.