Offline Apps: Harder Than It Looks

I was recently asked to create a web page that could be used as a sign (full screened browser) in a local festival. The data for the sign would be fed from a JSON API they had set up. Oh but the signs will be on wifi and since there will be thousands of people the wifi will be spotty at best so it needs to work offline as much as possible. For our signs to fully work offline we figured we would have to cache:

  • the .html file itself
  • all JS libs we used
  • any assets (images, stylesheets, etc) we use
  • any dynamic assets (as fed by the JSON API)
  • the results of the last successful call the the JSON API



To do this we ended up using 3 different browser caching mechanisms. I’m not going to spend a ton of time writing about each caching mechanism when there are great articles I can just link to, instead I’ll list what they cache and why we used them.

Let’s start with the obvious choice to enable offline support, the AppCache:

AppCache

Ideal for caching static files: .html, .js, .css, images.

What it solved for us:

Caching of almost all static resources (.html, .js, .css, most images)

What it didn’t solve:

Caching of the last successful JSON call
Caching of dynamic resources (can’t use wildcard in manifest file)

AppCache is pretty easy to use. You just put an additional manifest attribute on your html element:

<html manifest="example.appcache">
  ...
</html>

Then in your manifest file (typically has .appcache extension) you list every file that you want the browser to make available offline.

CACHE MANIFEST
# Here you list out every file you want available offline
index.html
cache.html
style.css
image1.png

The manifest file also provides for two additional sections NETWORK where you can specify resources that ALWAYS use the network and FALLBACK where you list what to do when an offline file is missing.

For more info on AppCache see this excellent MDN article: https://developer.mozilla.org/en-US/docs/HTML/Using_the_application_cache

localStorage

Ideal for caching data that can change

What it solved for us:

Saving the last known good JSON data and persisting it over page refreshes, browser crashes, etc

For our app we’re relying on a JSON API for all data but if the WIFI crapped out and somehow the page got refreshed we wanted to be able to show the last known data. Local Storage is perfect for this as it stores data as key/value pairs and persists even when the browser is refreshed or even closed.

//On initialization of the page pull out the last known good data
var lastGoodData = JSON.parse(localStorage.getItem('signageData'));

//On receiving new data we write it into the localStorage
localStorage.setItem('signageData', JSON.stringify(ourNewData));

Now if you’re observant you’re probably asking yourself why the JSON.parse and JSON.stringify calls are in that example. The reason is localStorage only stores strings. If you call localStorage.setItem(‘key’, myObject) it’ll call .toString() on your object and end up storing something similar to “[Object object]” in storage. Since we want to store data and not just a string we use the JSON library to convert our data into a JSON string before storing it and likewise when we pull it out of storage we have to call JSON.parse to convert it back to data.

For more information about localStorage see this article: https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Storage

FileSystem API (Chrome ONLY!)

Ideal for caching dynamic assets

Using the above two methods (which almost 100% work with all modern browsers) got us 95% of the way towards our offline goal. The next problem we encountered was that our JSON Data that comes back contains an array of images that we need to show. We don’t 100% know what the images will be ahead of time so we can’t specify them in AppCache and AppCache doesn’t support wildcard matching so we needed something different.

What we really wanted to do is download the files to the hard drive and serve them from there. We could have written a shell script or something similar and a cron job but we wanted to be part of our JavaScript solution. We came across Chrome’s FileSystem API and it seemed like a good fit. With the FileSystem API you can ask the browser to create a file sytem on the local hard drive like this:

var fileSystem = null;

window.webkitRequestFileSystem(
	PERSISTENT, //can also be TEMPORARY 
	numberOfBytes, 
	function(fs) {
		//success callback
		fileSystem = fs;		
	}, 
	errorHandler);

When this code runs you’ll see a little prompt at the top of your browser that says something like:

“notacog.com wants to permanently store large data on your local computer [OK] [Cancel]”

If the user clicks OK then the success callback is called and if they click Cancel the error callback is called.

When we get new JSON data we’re looping through it and downloading all of the images using an XHR request (assuming they don’t already exist on the fileSytem). Once we have the image data downloaded we call a method like this to save the file:

var imageData = //binary image data gotten from XHR request

saveFile(imageData, 'myImage.png', function(localURL) {
	//save the new localURL back to the collection so we can use
	//it to serve the image to the user
});

function saveFile(data, path, success) {
    if (!fileSystem) return;

    fileSystem.root.getFile(path, {create: true}, function(fileEntry) {
        fileEntry.createWriter(function(writer) {
            writer.write(data);
            success(fileEntry.toURL());
        }, errorHandler);
    }, errorHandler);
}

Once the file is saved we call fileEntry.toURL() on it which returns us a URL that we can put in our HTML to serve the file out of cache (something like: filesystem:http://localhost/persistent/myImage.png.

There are a couple of good articles on the FileSystem API:
- http://www.html5rocks.com/en/tutorials/file/filesystem/
- http://www.html5rocks.com/en/tutorials/file/filesystem-sync/ (for using it within a WebWorker)
- http://www.adobe.com/devnet/html5/articles/real-world-example-html5-filesystem-api.html

Leave a Reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>