SlideShare a Scribd company logo
Taking
web apps
offline

                        pedro@morais.it
                          @pedromorais
GDG Portugal DevFest   morais on app.net
     March ’13
Offline webapps - why?

 no network connection required
 faster startup
 keep data local
The Road to Offline


    user data

     assets
1. user data
localStorage
    *and sessionStorage
localstorage

              Simple API


window.localStorage[‘hello’] = ‘world’;
localStorage
       Can store a JSON string


window.localStorage[‘helloJSON’] =
 JSON.stringify({‘key1’: ‘world’,
       ‘key2’: ‘Lisbon’});
92.25%   http://caniuse.com/indexeddb
localStorage limitations
           Storage Limits


            Spec - 5 Mb
        Firefox - can adjusted
  Chrome - 2600k chars   (5 mb in UTF-16)

          IE - 5000k chars
localStorage limitations
              Sync API


   window.localStorage[‘sloooow’] =
     ‘imagine a 1 MB string here’;
sql storage
sql storage
     work stopped November ’10


 sqlite was used in all implementations
            (webkit, opera)


mozilla & microsoft: “not gonna happen”
48.32%   http://caniuse.com/sql-storage
sql storage

http://nparashuram.com/IndexedDBShim/


    indexed db polyfill over websql
indexed db
indexed db
                     Async API


var request = indexedDB.doStuff();
request.onerror = function(event) {
 ...
};
request.onsuccess = function(event) {
 ...
};
indexed db
               Opening a database


var request = indexedDB.open("MyTestDatabase");
request.onerror = ...
var db;
request.onsuccess = function(event) {
 db = request.result;
};
creating the schema

var request = indexedDB.open("MyTestDatabase", 5);
...
request.onupgradeneeded = function(event) {
 var db = event.target.result;
 ...
};
creating the schema
# student: cardNumber, name, email, ...
request.onupgradeneeded = function(event) {
 var db = event.target.result;
 var studentsStore = db.createObjectStore("students",
                               { keyPath: "cardNumber" });
     studentsStore.createIndex("nameIndex", "name",
                              { unique: false });
     studentsStore.createIndex("emailIndex", "email",
                              { unique: true });
};
adding data
var transaction
     = db.transaction(["students"], "readwrite");


transaction.oncomplete = function(event) {
  alert("All done!");
};


...
// transactions go away when you
// return to the event loop
// without making a request
adding data
...
var objectStore = transaction.objectStore("customers");
aStudent = { ‘studentCard’: ‘44124’,
             ‘name’: ‘Pedro Morais’,
             ‘email’: ‘pedro@morais.it'}


var request = objectStore.add(aStudent);
request.onsuccess = function(event) {
  // event.target.result == aStuddent.studentCard
};
updating data
...
var objectStore = transaction.objectStore("customers");
aStudent = { ‘studentCard’: ‘44124’,
             ‘name’: ‘Pedro Morais with updated name’,
             ‘email’: ‘pedro@morais.it'}


var request = objectStore.put(aStudent);
request.onsuccess = function(event) {
  // event.target.result == aStuddent.studentCard
};
deleting data

...
var objectStore = transaction.objectStore("customers");


var request = objectStore.delete(‘44124’);
request.onsuccess = function(event) {
 // deleted
};
getting data

var transaction = db.transaction(["students"]);
var objectStore = transaction.objectStore("customers");


var request = objectStore.get(‘44124’);
request.onsuccess = function(event) {
 console.log(“Name is “, request.result.name);
};
using a cursor
....


var request = objectStore.openCursor();
request.onsuccess = function(event) {
     var c = event.target.result;
  if (c) {
    console.log
          ("Student " + c + " is named " + c.value.name);
    cursor.continue();
  } else {
    console.log("Done!");
  }
};
using an index

...
var index = objectStore.index("name");
index.get("Pedro Morais").onsuccess = function(event) {
  console.log("Email=" + event.target.result.email);
};


// if name is not unique
// you get the first entry that matches
using an index + cursor
...
var index = objectStore.index("name");
var request
     = index.openCursor(IDBKeyRange.only("Pedro Morais"));
request.onsuccess = function(event) {
  var c = event.target.result;
  if (c) {
    // c.key is a name, like "Pedro Morais",
       // c.value is the whole object.
    console.log("Name: " + cursor.key +
                   “Email: " + cursor.value.email);
    cursor.continue();
  }
};
cursor key ranges

IDBKeyRange.only("Pedro Morais")
IDBKeyRange.lowerBound("Pedro")
IDBKeyRange.lowerBound("Pedro", true) // don’t inc Pedro
IDBKeyRange.upperBound("Pedro", true) // don’t inc Pedro


IDBKeyRange.bound("Pedro", "Vanda", false, true);
// include Pedro, don’t include Vanda
46.93%   http://caniuse.com/indexeddb
2. assets
offline web apps
67.43%   http://caniuse.com/offline-apps
offline web apps
<!DOCTYPE html>
<html manifest=”cache.appcache”>
...


  cache.appcache must be served as
       text/cache-manifest
cache manifest
CACHE MANIFEST
# this is a comment

css/styles.css
js/scripts.js
images/logo.png

NETWORK:
*
cache with fallback
CACHE MANIFEST
# this is a comment

css/styles.css
js/scripts.js
images/logo.png

FALLBACK:
/ /offline.html

NETWORK:
*
network access
                    not using appcache

• user navigates to http://test.com/app.html
• browser check if file “app.html” is in cache
• browser check if it has not expired
• browser checks validity of file using etags (optional)
• browser renders the cached or downloaded app.html
network access
                       using appcache

• user navigates to http://test.com/app.html
• browser renders app.html from appcache
• in the background:
  • browser checks if manifest has changed
  • if it has, browser downloads all files in the manifest
    (expires, etags still apply)

  • user only gets new version of app.html the next time!
cache manifest versions
 CACHE MANIFEST
 # version 50

 css/styles.css
 js/scripts.js
 images/logo.png

 NETWORK:
 *
cache manifest versions
 CACHE MANIFEST

 css/styles.css?b3c4de
 js/scripts.js?adf341
 images/logo.png?ef3451

 NETWORK:
 *


                          + far future expires
cache manifest versions
 CACHE MANIFEST

 b3c4de/css/styles.css
 adf341/js/scripts.js
 ef3451/images/logo.png

 NETWORK:
 *


                          + far future expires
conclusion
Available today

   localStorage
 Indexed DB (+ polyfill)
    AppCache

Take your web apps offline
Thanks!



                        pedro@morais.it
                          @pedromorais
GDG Portugal DevFest   morais on app.net
     March ’13

More Related Content

Taking Web Apps Offline

  • 1. Taking web apps offline pedro@morais.it @pedromorais GDG Portugal DevFest morais on app.net March ’13
  • 2. Offline webapps - why? no network connection required faster startup keep data local
  • 3. The Road to Offline user data assets
  • 5. localStorage *and sessionStorage
  • 6. localstorage Simple API window.localStorage[‘hello’] = ‘world’;
  • 7. localStorage Can store a JSON string window.localStorage[‘helloJSON’] = JSON.stringify({‘key1’: ‘world’, ‘key2’: ‘Lisbon’});
  • 8. 92.25% http://caniuse.com/indexeddb
  • 9. localStorage limitations Storage Limits Spec - 5 Mb Firefox - can adjusted Chrome - 2600k chars (5 mb in UTF-16) IE - 5000k chars
  • 10. localStorage limitations Sync API window.localStorage[‘sloooow’] = ‘imagine a 1 MB string here’;
  • 12. sql storage work stopped November ’10 sqlite was used in all implementations (webkit, opera) mozilla & microsoft: “not gonna happen”
  • 13. 48.32% http://caniuse.com/sql-storage
  • 14. sql storage http://nparashuram.com/IndexedDBShim/ indexed db polyfill over websql
  • 16. indexed db Async API var request = indexedDB.doStuff(); request.onerror = function(event) { ... }; request.onsuccess = function(event) { ... };
  • 17. indexed db Opening a database var request = indexedDB.open("MyTestDatabase"); request.onerror = ... var db; request.onsuccess = function(event) { db = request.result; };
  • 18. creating the schema var request = indexedDB.open("MyTestDatabase", 5); ... request.onupgradeneeded = function(event) { var db = event.target.result; ... };
  • 19. creating the schema # student: cardNumber, name, email, ... request.onupgradeneeded = function(event) { var db = event.target.result; var studentsStore = db.createObjectStore("students", { keyPath: "cardNumber" }); studentsStore.createIndex("nameIndex", "name", { unique: false }); studentsStore.createIndex("emailIndex", "email", { unique: true }); };
  • 20. adding data var transaction = db.transaction(["students"], "readwrite"); transaction.oncomplete = function(event) {   alert("All done!"); }; ... // transactions go away when you // return to the event loop // without making a request
  • 21. adding data ... var objectStore = transaction.objectStore("customers"); aStudent = { ‘studentCard’: ‘44124’, ‘name’: ‘Pedro Morais’, ‘email’: ‘pedro@morais.it'} var request = objectStore.add(aStudent); request.onsuccess = function(event) {   // event.target.result == aStuddent.studentCard };
  • 22. updating data ... var objectStore = transaction.objectStore("customers"); aStudent = { ‘studentCard’: ‘44124’, ‘name’: ‘Pedro Morais with updated name’, ‘email’: ‘pedro@morais.it'} var request = objectStore.put(aStudent); request.onsuccess = function(event) {   // event.target.result == aStuddent.studentCard };
  • 23. deleting data ... var objectStore = transaction.objectStore("customers"); var request = objectStore.delete(‘44124’); request.onsuccess = function(event) { // deleted };
  • 24. getting data var transaction = db.transaction(["students"]); var objectStore = transaction.objectStore("customers"); var request = objectStore.get(‘44124’); request.onsuccess = function(event) { console.log(“Name is “, request.result.name); };
  • 25. using a cursor .... var request = objectStore.openCursor(); request.onsuccess = function(event) { var c = event.target.result;   if (c) {     console.log ("Student " + c + " is named " + c.value.name);     cursor.continue();   } else {     console.log("Done!");   } };
  • 26. using an index ... var index = objectStore.index("name"); index.get("Pedro Morais").onsuccess = function(event) {   console.log("Email=" + event.target.result.email); }; // if name is not unique // you get the first entry that matches
  • 27. using an index + cursor ... var index = objectStore.index("name"); var request = index.openCursor(IDBKeyRange.only("Pedro Morais")); request.onsuccess = function(event) {   var c = event.target.result;   if (c) {     // c.key is a name, like "Pedro Morais", // c.value is the whole object.     console.log("Name: " + cursor.key + “Email: " + cursor.value.email);     cursor.continue();   } };
  • 28. cursor key ranges IDBKeyRange.only("Pedro Morais") IDBKeyRange.lowerBound("Pedro") IDBKeyRange.lowerBound("Pedro", true) // don’t inc Pedro IDBKeyRange.upperBound("Pedro", true) // don’t inc Pedro IDBKeyRange.bound("Pedro", "Vanda", false, true); // include Pedro, don’t include Vanda
  • 29. 46.93% http://caniuse.com/indexeddb
  • 32. 67.43% http://caniuse.com/offline-apps
  • 33. offline web apps <!DOCTYPE html> <html manifest=”cache.appcache”> ... cache.appcache must be served as text/cache-manifest
  • 34. cache manifest CACHE MANIFEST # this is a comment css/styles.css js/scripts.js images/logo.png NETWORK: *
  • 35. cache with fallback CACHE MANIFEST # this is a comment css/styles.css js/scripts.js images/logo.png FALLBACK: / /offline.html NETWORK: *
  • 36. network access not using appcache • user navigates to http://test.com/app.html • browser check if file “app.html” is in cache • browser check if it has not expired • browser checks validity of file using etags (optional) • browser renders the cached or downloaded app.html
  • 37. network access using appcache • user navigates to http://test.com/app.html • browser renders app.html from appcache • in the background: • browser checks if manifest has changed • if it has, browser downloads all files in the manifest (expires, etags still apply) • user only gets new version of app.html the next time!
  • 38. cache manifest versions CACHE MANIFEST # version 50 css/styles.css js/scripts.js images/logo.png NETWORK: *
  • 39. cache manifest versions CACHE MANIFEST css/styles.css?b3c4de js/scripts.js?adf341 images/logo.png?ef3451 NETWORK: * + far future expires
  • 40. cache manifest versions CACHE MANIFEST b3c4de/css/styles.css adf341/js/scripts.js ef3451/images/logo.png NETWORK: * + far future expires
  • 42. Available today localStorage Indexed DB (+ polyfill) AppCache Take your web apps offline
  • 43. Thanks! pedro@morais.it @pedromorais GDG Portugal DevFest morais on app.net March ’13