Web applications that wish to work robustly in flaky or offline scenarios can use client-side persistence to serve stale data while transparently trying to reconnect for more up-to-date data if possible.
In a mobile scenario, the user may consider himself “connected” when in fact he has dropped out of connectivity for a moment (for instance, he may have gone under a tunnel). Because of this, and because latency on mobile devices can be quite high, a well-behaved mobile web application (or even simple website) will serve up content out of a local cache, so the user can see it quickly, before trying to make a connection to retrieve new content.
This completely eliminates the fear of pressing the back button on mobile devices, since the user will be able to see the “index” page on a content site, for instance, even if their connection has temporarily dropped.
The jQuery offline plugin provides an easy mechanism for retrieving JSON data from a remote server, and then caching it. Subsequent requests for the same URL will retrieve the data from the cache, rather than the remote server.
If the user is online, the plugin will transparently request new content from the remote server, firing the callback again if the content has changed. If the user is offline, the plugin will request the data from the remote server for the most recent request when the user comes back online.
jQuery Offline uses the HTML5 localStorage
API for persistence. You can use the same API for browsers that do not support localStorage
, jQuery Offline will simply fall back to making a request to the server each time. As a result jQuery.retrieveJSON
is a portable way to make a request for JSON that should be cached if possible.
For more information on the basic strategy used here and the rationale for it, check out Rack::Offline, starting from “Application Cache”.
jQuery Offline can be used standalone with jQuery 1.4.2 and above. However, it is best used in conjunction with Rack::Offline and jquery-tmpl.
Rack::Offline
automates the process of generating a cache manifest, and jquery-tmpl
automates the process of taking a JavaScript object retrieved via jQuery.retrieveJSON
and making HTML out of it.
Note that neither iPhone OS 3.1 and earlier nor jQuery have native JSON serialization tools. You can grab json.js
from the lib
directory of this repository. It’s a small library, and it will not override the native serialization and deserialization if they exist (such as on recent versions of Firefox, Safari, and iPhone OS 3.2 and later).
You can find jQuery Offline in the lib
directory of this repository.
jQuery Offline provides two methods:
jQuery.retrieveJSON("/url", {data: "toSend"}, function(json, status, data) {
// json will be the same whether or not the cache was hit
// status will be either "success" or "cached"
})
jQuery.retrieveJSON
has the same API as jQuery.getJSON
with one exception: if the data is available in the cache, the callback will be called twice. First, it will be called with the object retrieved from the cache with the status “cached”. Next, once the JSON request succeeds, it will be called with the object returned from the Ajax request with the status “success”.
Returning false from the callback when the status is cached
will cause jQuery Offline to skip the Ajax request. You can use this if you don’t want to refresh the content if there is local content available.
If the contents come from the cache, the third parameter will be an object containing the time that jQuery Offline originally cached the content: { cachedAt: originalTime }
.
If the Ajax request was made after a successful hit from the cache, jQuery Offline will make the third parameter to the function { cachedAt: originalTime, retrievedAt: timeRetrieved }
. The originalTime
is the original time that the item was put into the cache. The timeRetrieved
is the time that the data was originally retrieved from the cache (in this session). You might use this third parameter to alert the user that a change is about to happen, if such a change would be particularly jarring.
Note that this third parameter is also supplied for a cache hit; if you want to determine whether a particular response is a follow-up to a cache hit, use: if( status == "success" && data )
.
Additionally, jQuery.retrieveJSON
will not make a new request if a request is in-process for a particular URL/query-string combination.
jQuery.clearJSON
takes the same first two parameters as jQuery.retrieveJSON
and clears the associated key. You can use this function to forcibly purge the cache for a particular URL and set of data. In general, you should not have to do this, as jQuery Offline will always make a follow-up request for content from the server if possible.
You should use jQuery Offline in combination with an HTML5 cache manifest and jquery-tmpl
for the best effect. An example follows.
First, the HTML:
<html manifest="application.manifest">
<head>
<link rel="stylesheet" href="/stylesheets/application.css" />
<script src="/javascripts/jquery.js"></script>
<script src="/javascripts/template.jquery.js"></script>
<script src="/javascripts/jquery.offline.js"></script>
<script id="articleTemplate" type="text/html">
{{each(article) articles}}
<article>
<header>
<h1>${article.title}</h1>
<h2>By ${article.byline}</h2>
</header>
${article.body}
</article>
{{/each}}
</script>
</head>
<body>
<div id="loading"><img src="loading.png" /></div>
<header>
<img src="masthead.png" />
<nav><ul>
<li>Main List</li>
<li>Recommended</li>
</ul></nav>
</header>
<div id="articles">
</div>
<footer>
Copyright Me, Inc.
</footer>
</body>
</html>
And the JavaScript using jquery-tmpl and jQuery Offline:
jQuery(document).ready(function($) {
// Since jQuery.retrieveJSON delegates to jQuery's Ajax
// to make requests, we can just set up normal jQuery
// Ajax listeners.
$("#loading").ajaxStart(function() { $(this).show(); });
$("#loading").ajaxStop(function() { $(this).hide(); });
var updateArticles = function(callback) {
$.retrieveJSON("/article_list.json", function(json, status) {
var content = $("#articleTemplate").render( json );
$("#articles").empty().append(content);
// If this *isn't* a cache hit, but rather a
// successful Ajax request, queue an update task
// for five minutes from now.
if( status == "success" ) { setTimeout( callback, 300000 ); }
});
};
// In five minutes, kick off a background request for
// more data. If the user is online, it will be processed
// immediately. If the user is not, it will queue the
// request for when the user comes online
setTimeout(function periodicUpdater() {
// Pass in this function as the callback to updateArticles
updateArticles(periodicUpdater);
}, 300000)
// Immediately try to retrieve the data. If the cached
// data is available, it will be used.
//
// If the user is online, it will kick off a request for
// updated content in the background. If not, it will
// queue the request for later.
updateArticles();
});
Because of the architecture of jQuery Offline, you can call $.retrieveJSON whether or not the user is online, and the plugin will either make an immediate request or kick off a request when the user comes back online. This also means you can kick off a timer for a request for new content in the callback to jQuery.retrieveJSON
. If the user is online, it’ll get new content every N minutes, like clockwork. If the user is offline, jQuery Offline will simply wait until the user comes back online to make the request.
The above JavaScript is roughly equivalent to the JavaScript strategy I used in Rack::Offline, but most of the dirty work is handled for you.
jQuery Offline has a test suite that tests functionality in browsers with localStorage
and browsers without localStorage
. To run the tests, you will need Ruby on your system. Then, follow these instructions in a checked out copy of this app:
$ gem install bundler $ bundle install $ bundle exec rackup & $ open http://localhost:9292/index.html # to run the localStorage-enabled tests $ open http://localhost:9292/index-fallback.html # to run the localStorage-disabled tests
jQuery Offline also passes JSLint.
Remember the time that the original content was cached; then provide it to follow-up calls- Provide better fallback for browsers without localStorage
- Provide more integrated support for jquery-templ?
- Deal with error messages because the cache is full
- LRU + preferred key algorithm
- Add TTL to the caching mechanism (each key or global?)