Working off the grid

HTML5 offline

  -  

Of course we're using Geolocation!

Who?

+


@


Slides: http://devfest-html5-offline.appspot.com

Summary

Why offline?

Before HTML5

Store Dynamic Data

Web Storage (localStorage/sessionStorage)

IndexedDB/WebSQL

localStorage

sessionStorage

window.sessionStorage

Web Storage APIs only store strings!

Solution:

var userstr = JSON.stringify({
  user: 'john',
  id: 10
});

localStorage.setItem('user', userstr);

var user = JSON.parse(localStorage.getItem('user'));

Same Origin Policy

  http://example.com:80/
    \       \         \_ port
     \       \_ domain
      \_ scheme
  

Example: Auto-save text

document.querySelector('#ta').addEventListener('keyup', function(e) {
  localStorage.setItem('value', this.value);
  localStorage.setItem('timestamp', (new Date()).getTime());
}, false);

What is IndexedDB?

IndexedDB

IndexedDB

// This is what our customer data looks like.
const customerData = [
  { ssn: "444-44-4444", name: "Bill", age: 35, email: "bill@company.com" },
  { ssn: "555-55-5555", name: "Donna", age: 32, email: "donna@home.org" }
];

var request = db.setVersion("1.0");
request.onerror = function(event) {
  // Handle errors.
};
request.onsuccess = function(event) {
  // Create an objectStore to hold information about our customers. We're
  // going to use "ssn" as our key path because it's guaranteed to be
  // unique.
  var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });

  // Create an index to search customers by name. We may have duplicates
  // so we can't use a unique index.
  objectStore.createIndex("name", "name", { unique: false });

  // Create an index to search customers by email. We want to ensure that
  // no two customers have the same email, so use a unique index.
  objectStore.createIndex("email", "email", { unique: true });

  // Store values in the newly created objectStore.
  for (i in customerData) {
    objectStore.add(customerData[i]);
  }
};
    

Finding things

Retrieving by key ( indexes ):

// db.createObjectStore("Friend", "id", true);
db.createIndex("FriendNames", "name", false);
var index = db.openIndex('FriendNames');
var id = index.get('Eric');

Querying ( cursors ):

// Restrict to names beginning A-E
var range = new KeyRange.bound('A', 'E');
var cursor = index.openObjectCursor(range);

while (cursor.continue()) {
  console.log(cursor.value.name);
}
var idbRequest = window.indexedDB.open('Database Name');
idbRequest.onsuccess = function(event) {
  var db = event.srcElement.result;
  var transaction = db.transaction([], IDBTransaction.READ_ONLY);
  var curRequest = transaction.objectStore('ObjectStore Name').openCursor();
  curRequest.onsuccess = ...;
};

WebSQL?

Store Static Resources

App Cache

App Cache

<html manifest="example.appcache">... </html>
CACHE MANIFEST
# 2010-11-17-v0.0.1

# Explicitly cached entries
CACHE:
index.html
stylesheet.css
images/logo.png

# static.html will be served if the user is offline
FALLBACK:
/ /static.html

# Resources that require the user to be online.
NETWORK:
*

App Cache Gotchas

Load twice workaround

applicationCache.addEventListener('updateready', function(e){
  if (applicationCache.status == 
         applicationCache.UPDATEREADY){
    if (confirm('Load new content?')) {
      ...
    }
  }
});

DEMO: Apps using offline features

Store Binary Data And Modify It

Opening the file system

window.requestFileSystem(
  TEMPORARY,        // persistent vs. temporary storage
  1024 * 1024,      // size (bytes) of needed space
  initFs,           // success callback
  opt_errorHandler  // opt. error callback, denial of access
);

1, 2, 3, Ways to generate URLs to files

var img = document.createElement('img');

// filesystem:http://example.com/temporary/myfile.png
img.src = fileEntry.toURL();
document.body.appendChild(img);

Retrieve a file by its filesystem URL:

window.resolveLocalFileSystemURL(img.src, function(fileEntry) { ... });

Fetching a file by name

function initFs(fs) {

  fs.root.getFile('logFile.txt', {create: true}, function(fileEntry) {

    // fileEntry.isFile == true
    // fileEntry.name == 'logFile.txt'
    // fileEntry.fullPath == '/logFile.txt'

    // Get a File obj
    fileEntry.file(function(file) { ... }, errorHandler);

    // fileEntry.remove(function() {}, errorHandler);
    // fileEntry.moveTo(...);
    // fileEntry.copyTo(...);
    // fileEntry.getParent(function(dirEntry) {}, errorHandler);

  }, errorHandler);

}

Duplicating user-dropped files

document.querySelector('#terminal').ondrop = function(e) {
  var files = e.dataTransfer.files;

  window.requestFileSystem(window.TEMPORARY, 1024*1024, function(fs) {

    Array.prototype.slice.call(files || [], 0).forEach(function(file, i) {
      fs.root.getFile(file.name, {create: true, exclusive: true}, function(fileEntry) {
        fileEntry.createWriter(function(fileWriter) {
          fileWriter.write(f); // Note: write() can take a File | Blob.
        }, errorHandler);

      }, errorHandler);
    });

  }, errorHandler);

};

DEMO: Peephole Extension


Example page

Install

Data Synchronization

Data Synchronization

navigator.onLine - know when you're all alone

if (navigator.onLine) {
  console.log('ONLINE!');
} else {
  console.log('Connection flaky');
}
window.addEventListener('online', function(e) {
  // Re-sync data with server.
}, false);

window.addEventListener('offline', function(e) {
  // Queue up events for server.
}, false);

Increase app performance

Use offline features to increase performance

Debug Offline Features

Quota on Google Chrome

Quotas

Default (Temporary)Quota Requested (Persistent)
Web Storage5MbN/A
App Cache10% of available disk in total
IndexedDB
WebSQL
File System APIArbitrary

Quota API

// Request Status
webkitStorageInfo.queryUsageAndQuota(webkitStorageInfo.TEMPORARY, 
    function(used, remaining) {
        console.log("Used quota: " + used + 
            ", remaining quota: " + remaining);
  }
);

// Request Quota (only for File System API)
webkitStorageInfo.requestQuota(webkitStorageInfo.PERSISTENT, 
    10 * 1024 * 1024, 
    function(used) {
        console.log("Used quota: " + used + 
            ", remaining quota: " + remaining);
  }
);

Quota API

Default (Temporary)Quota Requested (Persistent)
Web Storage5MbN/A
App Cache10% of available disk in total
Used: 0, bytesremaining: 3503554082 Bytes
IndexedDB
WebSQL
File System APIArbitrary
in Bytes Used: 1316134912 Bytes

Support

Web StorageYYYYY (8+)
IndexedDBYNYNN
WebSQLNYYYN
App CacheYYYYN
File System APINNYNN

Support (Mobile)

Web StorageYYY (2+)YY
IndexedDBYNNNN
WebSQLNYY (2+)YN
App CacheYYY (2.1+)YN
File System APINNY (3+)NN

Package your app to get more quota (if you need it)

For most apps, you can get them packaged up for distribution in a matter of a minute or two. Just go to appmator.com!

Because we're using the application cache, you may want to request the "unlimitedStorage" permission. Just paste it in manually to the manifest.json file.

Offlining your apps

{
  "name": "Great App Name",
  "description": "Pithy description",
  "version": "0.0.0.1",
  "icons": {
    "128": "icon_128.png"
  },
  permissions : [ "unlimitedStorage" ],
  "offline_enabled": true,
  "app": {
    ...
  }
}

Polyfills

Web Storage (LocalStorage and SessionStorage)

More Resources

Thank you!

Questions?