Offline. Na und?

Stephan Hochdörfer // 01.07.2014

Über mich

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

  • #PHP, #DevOps, #Automation

Das "neue" Zeitalter

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

Die noBackend Bewegung!




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

Wie mit "offline" umgehen?







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

Ist HTML5 nicht nutzbar?

Kann mehr als man denkt!

Was heißt "offline"?




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

Offline Container




  • Hybrid Container
  • App Cache

Applikation: Hybrid Container

Applikation: 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
#2014-05-22

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

App Cache

CACHE MANIFEST
# 2014-05-22

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
# 2014-05-22

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: Fallstricke





Dateien werden immer(!)
vom lokalen Cache ausgeliefert.

App Cache: Fallstricke





Der lokale Cache wird nur dann aktualisiert
wenn sich die manifest Datei geändert hat.

App Cache: Fallstricke





Nicht ladbare Dateien aus der CACHE Sektion
führen dazu dass der Cache invalide ist.

App Cache: Fallstricke





Kann die manifest Datei nicht geladen werden,
erfolgt kein Caching!

App Cache: Fallstricke





Nicht gecachte Ressourcen werden auf
einer gecachten Seite nicht angezeigt.

App Cache: Fallstricke





Nach Aktualisierung des Caches muss
die Seite neu geladen werden!

App Cache: Fallstricke





Mit expires Header arbeiten um das
Cachen des manifests zu verhinden!

App Cache: Validator





http://manifest-validator.com/

Was darf gecacht werden?


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

Was sollte (nicht) gecacht werden?



  • CSS
  • HTML
  • Javascript

Data URI Schema




« [...] 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 Schema

<!DOCTYPE HTML>
<html>
 <head>
  <title>Data URI Schema</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 Schema

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

Data URI: Fallstricke





Nicht getrennt vom Dokument. Dokument
muss jedes Mal neu geladen werden.

Data URI: Fallstricke





Ändert sich der Inhalt muss dieser
erneut codiert und eingebettet werden.

Data URI: Fallstricke





Kein Support für IE7.
IE8 erlaubt max. 30K an Daten.

Data URI: Fallstricke





Base64 kodierte Strings sind um etwa
1/3 größer als die binäre Repräsentation.

Wie Daten lokal speichern?



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

Beispiel: Todoliste

Web Storage





Komfortable Art Daten offline zu
speichern: Key/Value Speicher

Web Storage: 2 Arten





localStorage vs. sessionStorage

Datensatz hinzufügen

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);
  }
}

Datensatz modifizieren

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

Datensatz löschen

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

Datensätze auslesen

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);
  }
}

Wie sessionStorage nutzen?





Ersetze „localStorage“ durch „sessionStorage“

Wie sessionStorage nutzen?

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);
  }
}

Web Storage Zugriff

var value = "my value";

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

// Array accessor
localStorage[key] = value;

// Property accessor
localStorage.key = value;

Web Storage: Vorteile




  • Breite Unterstützung von Browsern
  • Einfach zu verwenden
  • Kein HTTP Overhead

Web Storage: Nachteile


  • Daten liegen unstruktriert vor
  • Keine Transaktionalität!
  • Veraltete Daten werden
    nicht automatisch gelöscht
  • Storage Quota kann nicht
    abgefragt werden
  • http:// and https:// definieren
    unterschiedliche Speicher!

Web SQL Database





Eine lokale SQL Datenbank auf SQLite Basis.

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));
  }
};

Datenbank erzeugen

// 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
  );
});

Datenbank Migration

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);
}

Datensatz hinzufügen

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

Datensatz modifizieren

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

Datensatz löschen

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

Datensätze auslesen

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

Vorteile




  • Wir haben eine SQL Datenbank
    im Browser!

Nachteile



  • Wir haben eine SQL Datenbank
    im Browser!
  • SQLite kann langsam sein!
  • Nicht länger Teil der
    HTML5 Spezifikation!

IndexedDB





Kompromiss aus Web Storage
und Web SQL Database.

"Normalisierungsarbeiten"

// 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;

Datenspeicher anlegen

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() {};
    };
  }
};

Datensatz hinzufügen

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);
  }
}

Datensatz modifizieren

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);
  }
}

Datensatz löschen

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);
  }
}

Datensätze auslesen

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);
  }
}

Lesen mittels Index

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

Index erzeugen

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);
}

Zu kompliziert?





db.js als Wrapper verwenden!

db.js als Wrapper

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

db.js als Wrapper

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

db.js als Wrapper

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

IndexedDB: Vorteile




  • Der "neue" Standard

IndexedDB: Nachteile




  • Nicht alle (mobilen) browser
    unterstützen IndexedDB!

File API




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

Vorbereitungen

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);
};

Vorbereitungen

// 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

Quota anfragen

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

      }
    }
  }
}

Quota anfragen

Daten speichern

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

Daten speichern

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
  );
}

Daten lesen

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

Daten lesen

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
  );
}

Browserunterstützung?

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 - -

Speicherplatzlimitierung?

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 ? ?

Bin ich online?

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

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

Bin ich online?

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

Wie Daten synchronisieren?





PouchDB, the JavaScript Database that syncs!

Wie Daten synchronisieren?

var db = new PouchDB('todo');

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

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







Vielen Dank!