HTML5 offline mobile apps:
Real world insights

Stephan Hochdörfer // 28.06.2014

About me

  • Stephan Hochdörfer
  • Head of IT, bitExpert AG (Mannheim, Germany)
  • S.Hochdoerfer@bitExpert.de
  • @shochdoerfer

  • #PHP, #DevOps, #Automation

The age of smartphones

Offline First!




« We live in a disconnected & battery powered world, but our technology and best practices are a leftover from the always connected & steadily powered past. »
- offlinefirst.org

Build apps with noBackend!




« [...] frontend developers build full stack apps,
without thinking about the backend. »
- nobackend.org

How to deal with "offline"?







« [...] we take the next step, announcing 2014
as the target for Recommendation. »
- Jeff Jaffe, CEO, W3C

HTML5 not ready?

HTML5 is ready!

What does "offline" mean?




« Offline storage is about capturing specific data generated by the user, or resources the user has expressed interest in. » - Eric Bidelman

Offline Containers




  • Hybrid Container
  • App Cache

Application: Hybrid Container

Application: App Cache




« The cache manifest in HTML5 is a software storage feature which provides the ability to access a web application even without a network connection. »
- Wikipedia

App Cache

<!DOCTYPE html>
<html lang="en">

App Cache

<!DOCTYPE html>
<html lang="en" manifest="cache.manifest">
CACHE MANIFEST
#2011-13-14

js/app.js
css/app.css
favicon.ico
http://someotherdomain.com/image.png

App Cache

CACHE MANIFEST
# 2013-11-14

NETWORK:
data.php

CACHE:
/main/home
/main/app.js
/settings/home
/settings/app.js
http://myhost/logo.png
http://myhost/check.png
http://myhost/cross.png

App Cache

CACHE MANIFEST
# 2013-11-14

FALLBACK:
/ /offline.html

NETWORK:
*

App Cache: Javascript

// events fired by window.applicationCache
window.applicationCache.onchecking = function(e)  {
    log("Checking for updates");
}

window.applicationCache.onnoupdate = function(e) {
    log("No updates");
}

window.applicationCache.onupdateready = function(e) {
    log("Update ready");
}

window.applicationCache.onobsolete = function(e) {
    log("Obsolete");
}

App Cache: Javascript

window.applicationCache.ondownloading = function(e) {
    log("Downloading");
}

window.applicationCache.oncached = function(e) {
    log("Cached");
}

window.applicationCache.onerror = function(e) {
    log("Error");
}

window.applicationCache.onprogress = function(e) {
  log("Progress: Downloading file " + counter++);
};

App Cache: Javascript

// Check if a new cache version is available
window.addEventListener('load', function(e) {
  
  window.applicationCache.addEventListener('updateready', 
      function(e) {
    
      if(window.applicationCache.status == 
          window.applicationCache.UPDATEREADY) {
      
          window.applicationCache.swapCache();
      
          if (confirm('New version is available. Load it?')) {
	      window.location.reload();
          }
      }

  }, false);

}, false);

App Cache: Some Gotchas





Files are always(!) served from the application cache.

App Cache: Some Gotchas





The application cache only updates if the
content of the manifest itself has changed!

App Cache: Some Gotchas





If any of the files listed in the CACHE section can't be retrieved, the entire cache will be disregarded.

App Cache: Some Gotchas





If the manifest file itself can't be retrieved,
the cache will ignored!

App Cache: Some Gotchas





Non-cached resources will not load on a cached page!

App Cache: Some Gotchas





The page needs to be reloaded,
otherwise the new resources do not show up!

App Cache: Some Gotchas





To avoid the risk of caching
manifest files set expires headers!

App Cache: Validation





http://manifest-validator.com/

What to cache?


  • Fonts
  • Splash image
  • App icon
  • Entry page
  • Fallback bootstrap

What (not) to cache?



  • CSS
  • HTML
  • Javascript

Data URI scheme




« [...] is a URI scheme (Uniform Resource Identifier scheme) that provides a way to include data in-line
in web pages as if they were external resources. »
- Wikipedia

Data URI scheme

<!DOCTYPE HTML>
<html>
 <head>
  <title>The Data URI scheme</title>
  <style type="text/css">
  ul.checklist li {
    margin-left: 20px;
    background: white url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==') no-repeat scroll left top;
  }
  </style>
 </head>
 <body>
 </body>
</html>

Data URI scheme

<!DOCTYPE HTML>
<html>
 <head>
  <title>The Data URI scheme</title>
 </head>
 <body>
  <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==" alt="Red dot">
 </body>
</html>

Data URI: Some Gotchas





Data URIs are not separately cached from their containing documents. The encoded data is downloaded every time the containing documents are re-downloaded.

Data URI: Some Gotchas





Content must be re-encoded and re-embedded
every time a change is made.

Data URI: Some Gotchas





Internet Explorer 7 lacks support.
Internet Explorer 8 limits the data size to 32K.

Data URI: Some Gotchas





Base64-encoded data URIs are 1/3 times
larger in size than their binary equivalent.

How to store data?



  • Web Storage
  • Web SQL Database
  • IndexedDB
  • File API

Example: Todolist application

Web Storage





Very convenient form of offline storage:
simple key-value store

Web Storage: 2 types





localStorage vs. sessionStorage

Web Storage: Add item

function add(item) {
  try {
    // for a new item set id
    if((typeof item.id === "undefined") 
      || (null == item.id) || ("" == item.id)) {
	item.id = get_lastIndex() + 1;
    }

    // store object as string
    localStorage.setItem(item.id, 
	JSON.stringify(item)
    );

    // update the index
    set_lastIndex(item.id);
  }
  catch(ex) {
    console.log(ex);
  }
}

Web Storage: Modify item

function modify(item) {
  try {
    // store object as string
    localStorage.setItem(item.id, 
      JSON.stringify(item)
    );
  }
  catch(ex) {
    console.log(ex);
  }
}

Web Storage: Remove item

function remove (id) {
  try {
    localStorage.removeItem(id);
  }
  catch(ex) {
    console.log(ex);
  }
}

Web Storage: Read items

function read() {
  try {
    var lastIdx = get_lastIndex();
    for(var i = 1; i <= lastIdx; i++) {
      if(null !== localStorage.getItem(i)) {
	
	// parse and render item
	var item = JSON.parse(
	  localStorage.getItem(i)
	);
      }
    }
  }
  catch(ex) {
    console.log(ex);
  }
}

What about sessionStorage?





Replace „localStorage“ with „sessionStorage“

What about sessionStorage?

function add(item) {
  try {
    // for a new item set id
    if((typeof item.id === "undefined") 
      || (null == item.id) || ("" == item.id)) {
      item.id = get_lastIndex() + 1;
    }

    // store object as string
    sessionStorage.setItem(item.id, 
      JSON.stringify(item)
    );

    // update the index
    set_lastIndex(item.id);
  }
  catch(ex) {
    console.log(ex);
  }
}

Don`t like method calls?

var value = "my value";

// method call
localStorage.setItem("key", value);

// Array accessor
localStorage[key] = value;

// Property accessor
localStorage.key = value;

Web Storage: Pro




  • Most compatible format up to now
  • Easy to use
  • No HTTP overhead

Web Storage: Con


  • The data is not structured
  • No transaction support!
  • Lack of automatically
    expiring storage
  • Inadequate information about
    storage quota
  • http:// and https:// define
    different stores!

Web SQL Database




An offline SQL database based on SQLite,
a general-purpose SQL engine.

Web SQL: Callbacks

var onError = function(tx, ex) {
  alert("Error: " + ex.message);
};

var onSuccess = function(tx, results) {
  var len = results.rows.length;

  for(var i = 0; i < len; i++) {

    // render found todo item
    render(results.rows.item(i));
  }
};

Web SQL: Setup database

// initalize the database connection
var db = openDatabase('todo', '1.0', 'Todo Database', 
   5 * 1024 * 1024 );

db.transaction(function (tx) {
  tx.executeSql(
    'CREATE TABLE IF NOT EXISTS todo '+ 
    '(id INTEGER PRIMARY KEY ASC, todo TEXT)',
    [], 
    onSuccess, 
    onError
  );
});

Web SQL: Migrations

function m1(t){ t.executeSql("create table tbl1...") }
function m2(t){ t.executeSql("alter table tbl1...") }
function m3(t){ t.executeSql("alter table tbl1...") }
 
if(db.version == "") {
  db.changeVersion("", "1", m1, null, function() {
    db.changeVersion("1", "2", m2, null, function() {
      db.changeVersion("2", "3", m3);
    });
  });
}
 
if(db.version == "1") {
  db.changeVersion("1", "2", m2, null, function() {
    db.changeVersion("2", "3", m3);
  });
}
 
if(db.version == "2") {
  db.changeVersion("2", "3", m3);
}

Web SQL: Add item

function add(item) {
  db.transaction(function(tx) {
    tx.executeSql(
      'INSERT INTO todo (todo) VALUES (?)',
      [
	item.todo
      ],
      onSuccess,
      onError
    );
  });
}

Web SQL: Modify item

function modify(item) {
  db.transaction(function(tx) {
    tx.executeSql(
      'UPDATE todo SET todo = ? WHERE id = ?',
      [
	item.todo
	item.id
      ],
      onSuccess,
      onError
    );
  });
}

Web SQL: Remove item

function remove(id) {
  db.transaction(function (tx) {
    tx.executeSql(
      'DELETE FROM todo WHERE id = ?',
      [
	id
      ],
      onSuccess,
      onError
    );
  });
}

Web SQL: Read items

function read() {
    db.transaction(function (tx) {
	tx.executeSql(
	    'SELECT * FROM todo',
	    [],
	    onSuccess,
	    onError
	);
    });
}

Web SQL: Pro




  • It`s a SQL database within
    the browser!

Web SQL: Con



  • It`s a SQL database within
    the browser!
  • SQLite can be slooow!
  • The specification is no longer
    part of HTML5!

IndexedDB




A nice compromise between Web Storage
and Web SQL Database giving you the
best of both worlds.

IndexedDB: Preparations

// different browsers, different naming conventions
var indexedDB = window.indexedDB || 
   window.webkitIndexedDB || window.mozIndexedDB || 
   window.msIndexedDB;

var IDBTransaction = window.IDBTransaction ||
   window.webkitIDBTransaction;

var IDBKeyRange = window.IDBKeyRange || 
   window.webkitIDBKeyRange;

IndexedDB: Create store

var db = null;
var request = indexedDB.open("todo");
request.onfailure = onError;
request.onsuccess = function(e) {
  db = request.result;
  var v = "1.0";
  if(v != db.version) {
    var verRequest = db.setVersion(v);
    verRequest.onfailure = onError;
    verRequest.onsuccess = function(e) {
      var store = db.createObjectStore("todo",
	{
	  keyPath: "id",
	  autoIncrement: true
	});

      e.target.transaction.oncomplete =  function() {};
    };
  }
};

IndexedDB: Add item

function add(item) {
  try {
    var trans = db.transaction(["todo"], 
      IDBTransaction.READ_WRITE);

    var store   = trans.objectStore("todo");
    var request = store.put({
      "todo": item.todo,
    });
  }
  catch(ex) {
    onError(ex);
  }
}

IndexedDB: Modify item

function modify(item) {
  try {
    var trans = db.transaction(["todo"], 
      IDBTransaction.READ_WRITE);

    var store   = trans.objectStore("todo");
    var request = store.put(item);
  }
  catch(ex) {
    onError(ex);
  }
}

IndexedDB: Remove item

function remove(id) {
  try {
    var trans = db.transaction(["todo"],
      IDBTransaction.READ_WRITE);

    var store   = trans.objectStore("todo");
    var request = store.delete(id);
  }
  catch(ex) {
    onError(ex);
  }
}

IndexedDB: Read items

function read () {
  try {
    var trans = db.transaction(["todo"],  IDBTransaction.READ);
    var store = trans.objectStore("todo");
    var keyRange = IDBKeyRange.lowerBound(0);
    var cursorRequest = store.openCursor(keyRange);
    cursorRequest.onsuccess = function(e) {
      var result = e.target.result;
      if(!!result == false) {
	return;
      }
      // @TODO: render result.value
      result.continue();
    };
  }
  catch(ex) {
    onError(ex);
  }
}

IndexedDB: Read by index

try {
  var index = db.openIndex('todo');
  var item  = index.get(1337);
}
catch(ex) {
  onError(ex);
}

IndexedDB: Create indexes

try {
  // create non-unique index 
  db.createIndex("Created", "createdDate", 
  {"unique": false});

  // create unique index 
  db.createIndex("OtherKey", "otherField", 
  {"unique": true});  

  // create multi-column index (not working in IE10!) 
  db.createIndex("MultiIndex", ["field1", "field2"]);  
}
catch(ex) {
  onError(ex);
}

Too complicated?





Use db.js as a wrapper!

IndexedDB: db.js

var server;
db.open( {
    server: 'todo',
    version: 1,
    schema: {
      todo: {
	  key: { keyPath: 'id' , autoIncrement: true},
	  indexes: {
	      Created: { }
	  }
      }
    }
}).done(function(s) {
    server = s;
});

IndexedDB: db.js

server.todo.add( {
  todo: 'This is a sample todo item'
}).done(function(item) {
  // item stored
});

IndexedDB: db.js

server.todo.query()
  .execute()
  .done(function(results) {
      // do something with the results
  });

IndexedDB: Pro




  • The "new" standard

IndexedDB: Con




  • Not all mobile browsers support it!

File API




« [...] provides an API for representing file objects in web applications and programmatic selection and accessing their data. » - Wikipedia

File API: Preparations

var onError = function(e) {
  var msg = '';

  switch(e.code) {
    case FileError.QUOTA_EXCEEDED_ERR:
      msg = 'QUOTA_EXCEEDED_ERR'; break;
    case FileError.NOT_FOUND_ERR:
      msg = 'NOT_FOUND_ERR'; break;
    case FileError.SECURITY_ERR:
      msg = 'SECURITY_ERR'; break;
    case FileError.INVALID_MODIFICATION_ERR:
      msg = 'INVALID_MODIFICATION_ERR'; break;
    case FileError.INVALID_STATE_ERR:
      msg = 'INVALID_STATE_ERR'; break;
    default:
      msg = 'Unknown Error'; break;
  };

  alert("Error: " + msg);
};

File API: Preparations

// File system has been prefixed as of Google Chrome 12
window.requestFileSystem = window.requestFileSystem ||
  window.webkitRequestFileSystem;

window.BlobBuilder = window.BlobBuilder || 
  window.WebKitBlobBuilder;

var size = 5 * 1024*1024; // 5MB

File API: Requesting quota

// request quota for persistent store
window.webkitStorageInfo.requestQuota(
  PERSISTENT,
  size,
  function(grantedBytes) {
    window.requestFileSystem(
      PERSISTENT,
      grantedBytes,
      function(fs) {
	
	// @TODO: access filesystem

      }
    }
  }
}

File API: Requesting quota

File API: Add item

function add(item) {
  window.webkitStorageInfo.requestQuota(
    PERSISTENT,
    size,
    function(grantedBytes) {
      window.requestFileSystem(
	PERSISTENT,
	grantedBytes,
	function(fs) {
	  writeToFile(fs, item);
	},
	onError
      );
    },
    function(e) {
      onError(e);
    }
  );
}

File API: Add item

function writeToFile(fs, item) {
  fs.root.getFile(
    'todo.txt',
    {
      create: true
    },
    function(fileEntry) {
      fileEntry.createWriter(
	function(fileWriter) {
	  var blob = new Blob([JSON.stringify(item)+"\n"]);

	  fileWriter.seek(fileWriter.length);
	  fileWriter.write(blob);
	}, 
	onError
      );
    }, onError
  );
}

File API: Read items

function read() {
  window.webkitStorageInfo.requestQuota(
    PERSISTENT,
    size,
    function(grantedBytes) {
      window.requestFileSystem(
	PERSISTENT,
	grantedBytes,
	function(fs){
	  readFromFile(fs);
	},
	onError
      );
    },
    function(e) {
      onError(e);
    }
  );
}

File API: Read items

function readFromFile(fs) {
  fs.root.getFile(
    'todo.txt',
    {
      create: true
    },
    function(fileEntry) {
      fileEntry.file(function(file){
	var reader = new FileReader();
	reader.onloadend = function(e) {
	  if (evt.target.readyState == FileReader.DONE) {
	    // process this.result
	  }
	};

	reader.readAsText(file);
      });
    }, 
    onError
  );
}

Browser support?

Web Storage Web SQL DB IndexedDB File API
IE 8.0 10.0 10.0 -
Firefox 11.0 11.0 11.0 19.0
Chrome 18.0 18.0 18.0 18
Safari 5.1 5.1 - -
iOS 3.2 3.2 - -
Android 2.1 2.1 - -

Storage limitations?

Web Storage Web SQL DB IndexedDB File API
IE 10 MB 500 MB 500 MB
Firefox 10 MB 50 MB 50 MB
Chrome 5 MB
Safari 5 MB 5 MB 5 MB
iOS 5 MB 5 MB 5 MB
Android 5 MB ? ?

Am I online?

document.body.addEventListener("online", function () {
  // browser is online!
}

document.body.addEventListener("offline", function () {
  // browser is not online!
}

Am I online?

$.ajax({
  dataType: 'json',
  url: 'http://myapp.com/ping',
  success: function(data){
    // ping worked
  },
  error: function() {
    // ping failed -> Server not reachable
  }
});

How to sync your data?





PouchDB, the JavaScript Database that syncs!

How to sync your data?

var db = new PouchDB('todo');

db.put({
 _id: 1,
 todo: 'Get some work done...',
});

db.replicate.to('http://myapp.com/mydb');







Thank you!








Do not forget to rate the talk:
https://joind.in/10903