Johan Broddfelt
/* Comments on code */

Offline Web Application

Check out my sampe here

Background

The discussions run wild on the Internet on whether to write native apps for each mobile platform or only build one web app that works on all. By building a native app you get more control from the device. You get cleared for access to gyro, gps, camera and so on while you accept to install the app. While with a web application you need to ask permission every time you use it and the support for hardware is often more stable if you run native. But you get access to more and more even from the browser. And in this post I'm going to show you how you can create a web page that can be run offline, even if it has a remote data source.

Technologies

Apart from the obvious HTML and JavaScript you need something called appcache in order to tell the browser to save some files locally. Then you also need to access the indexedDB in order to store data from your remote data source.

First let's take a look at the index.html file

// index.html
<!DOCTYPE html>
<html manifest="./cache.manifest">
    <head>
        <meta name="viewport" content="minimal-ui, width=device-width, initial-scale=1, maximum-scale=1, maximum-scale=1, user-scalable=no">
        <meta http-equiv="x-ua-compatible" content="IE=edge" />
    </head>
    <body>
        <script src="./indexeddb.shim.min.js"></script>
        <script src="./application.js"></script>
    </body>
</html>

Here you can see that I have included the cache.manifest file. This file contains the files that the browser should save for offline use. Then you can see that I have included a indexeddb.shim.min.js. This is so that I have a fallback for browsers that does not support indexeddb. I really recommend using this because there is still some differences in support when it comes to browser data storage solutions. But from what I can read out there this should get you covered in most browsers.
Then I have an application.js file which contain my javascript code.

Application Cache

Let's have a look in the manifest file for the Application Cache

// cache.manifest
CACHE MANIFEST
NETWORK:
*
CACHE:
application.js

FALLBACK:
#not_available_offline.html

The * under NETWORK means that I cache all urls that I need for the site to run, but if I am online the online sources will be used. Then I explicitly cache application.js so that I always use the cached version. I guess I do not need to do that, but I'm just experimenting a bit here and showing you the options. So bare with me =)

At the end there is also a fallback if the browser does not support appcache

IndexedDB and javascript

Now take a look at the JavaScript. Everything starts at the databaseOpen function. It opens the indxedDB, checks if an update is needed and calls the function getSiteObject.


document.addEventListener("DOMContentLoaded", function(){
    if ("indexedDB" in window) {
        //alert("YES!!! I CAN DO IT!!! WOOT!!!");
    } else {
        //alert("I has a sad.");
    }
    // General functions
    ajaxRequest = function() {
        var activexmodes=["Msxml2.XMLHTTP", "Microsoft.XMLHTTP"]; //activeX versions to check for in IE
        if (window.ActiveXObject) { //Test for support for ActiveXObject in IE first (as XMLHttpRequest in IE7 is broken)
            for (var i=0; i<activexmodes.length; i++) {
                try {
                    return new ActiveXObject(activexmodes[i]);
                } catch(e) {
                    //suppress error
                }
            }
        } else if (window.XMLHttpRequest) { // if Mozilla, Safari etc 
            return new XMLHttpRequest();
        } else {
            //var commentBox = document.getElementById('news_comments');
            //commentBox.innerHTML = 'Could not connect to server. Your browser does not support XMLHttpRequest!';
            alert('Could not connect to server. Your browser does not support XMLHttpRequest!');
            return false;
        }
    };
    queryServer = function(params, url, callback) {
        var http = new ajaxRequest();
        http.open("POST", url, true);

        //Send the proper header information along with the request
        http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        //http.setRequestHeader("Content-length", params.length);
        //http.setRequestHeader("Connection", "close");

        http.onreadystatechange = function() { //Call a function when the state changes.
          if (parseInt(http.readyState) === 4 && parseInt(http.status) === 200) {
            var jsonData = JSON.parse(http.responseText);
            if (jsonData.message !== undefined && jsonData.message !== '') {
                alert(jsonData.message);
            }
            if (jsonData.status === 0) {
                logout();
            } else {
                callback(jsonData);
            }
          } else {
            if (parseInt(http.status) !== 200) {
                //callback(jsonData);
            }
          }
        }
        http.send(params);
    };

    // indexedDB.deleteDatabase('todos'); // To clear the database instead of incrementing the version number...
    // 'global' variable to store reference to the database
    var db;

    databaseOpen(function() {
            //console.log("The database has been opened");
            getSiteObj(gotData);
    });

    function databaseOpen(callback) {
        // Open a database, specify the name and version
        var version = 4;
        var request = indexedDB.open('todos', version);

        // Run migrations if necessary
        request.onupgradeneeded = function(e) {
            //console.log('onupgradeneeded');
            db = e.target.result;
            e.target.transaction.onerror = databaseError;
            db.createObjectStore('todo2');
        };

        request.onsuccess = function(e) {
            //console.log('success');
            db = e.target.result;
            callback();
        };
        request.onerror = databaseError;
    }

    function databaseError(e) {
            console.error('An IndexedDB error has occurred', e);
    }

/*			
    function setSiteObj(jsonStr) {
            window.localStorage.setItem('site_data', jsonStr);
    }
    function getSiteObj() {
            return window.localStorage.getItem('site_data');
    }
*/
    function setSiteObj(jsonStr, callback) {
        var transaction = db.transaction(['todo2'], 'readwrite');
        var store = transaction.objectStore('todo2');
        //console.dir(store);
        var request = store.put(jsonStr, "myData");
        transaction.oncomplete = function(e) {
            if (callback !== undefined) {
                callback();
            }
        };
        request.onerror = databaseError;
    }
    function getSiteObj(callback) {
        var transaction = db.transaction(['todo2'], 'readonly');
        var store = transaction.objectStore('todo2');

        // Get everything in the store
        var keyRange = IDBKeyRange.lowerBound(0);
        var cursorRequest = store.openCursor(keyRange);

        // This fires once per row in the store. So, for simplicity,
        // collect the data in an array (data), and pass it in the
        // callback in one go.
        var data = [];
        cursorRequest.onsuccess = function(e) {
            var result = e.target.result;

            // If there's data, add it to array
            if (result) {
                console.dir(result);
                data.push({'key':result.key, 'val':result.value});
                result.continue();

                // Reach the end of the data
            } else {
                callback(data);
            }
        };
    }

    // Read all data you can get from localStorage
    var siteData = '';
    function gotData(data) {
        //console.log('gotData: ');
        //console.dir(data);
        var i = 0;
        var res = '';
        while (i < data.length) {
            if (data[i].key === "myData") {
                //console.log(data[i]);
                res = data[i].val;
            }
            i++;
        }
        render(res);
    }


    function render(siteData) {
        //console.dir(siteData);
        // Render site with that data
        document.body.innerHTML = '<a href="./">Reload</a><br>Data: ' + siteData;

        // Try to get updated data from the server

        var url = "ajax.php";
        updateData = function(jsonData) {
            if (jsonData.data !== undefined) {
                var siteDataNew = jsonData.data;

                // Update the current layout with the new data
                document.body.innerHTML += ' Updated from server: ' + siteDataNew;
                // Update the localStorage
                setSiteObj(siteDataNew);
            } else {
                document.body.innerHTML += ' Server unavailable';
            }
        };
        queryServer("", url, updateData);
    }
},false);

As you can see in the render function at the bottom we start out by reading data from the indexedDB data source and then we try to access our server to fetch some updated data. If that fails we still have our cached version and if we get the new data then the view and the data source is updated.

In this sample I only use a random number generator as a data source. Here is the code for that one.

// ajax.php
<?php
echo '{"data": "' . rand(0, 10) . '", "status": 1}';

And you might also want to update your .htaccess file with the following line:

// .htaccess
AddType text/cache-manifest .manifest

Current thoughts

The reason why I went back to this old code is because I'm trying to rewrite a website I have, in order to make it more mobile friendly. But I struggle a bit, because I do not only want to rely on this solution. I also want to site available without any app cache and JavaScript updating the interface. I need the page to load with data but not if the user have data in their app cache. But the server does not know that without talking to the client first. I'm not sure how well google manages data that are loaded dynamically. Probably okej but I do not want to take any chanses.
I do not want a complete copy of my database locally, but I still want enough data so that I can use the site offline. One of the major benefits is that I can remove a lot of load from the server with this method. I'm thinking about storing the same data structure in the SESSION variable in php, as a fallback from the app cache.
There are a lot of questions here and I'm sure most of them will turn out to be easier than I thought, so I should just start coding and stop procastenating...

- Mobile, Application Cache, IndexedDB

Follow using RSS

<< Philosophy of a programmer Obstetric pain timer >>

Comment

Name
Mail (Not public)
Send mail uppdates on new comments
0 comment