Upload
jeromevdl
View
62
Download
4
Embed Size (px)
Citation preview
Offline applications
Jérôme Van Der Linden - 28/10/2016
jeromevdl @jeromevdl
Jérôme Van Der Linden
@OCTOSuisse @OCTOTechnology
AT AW AD
AnyWhere ?
AnyWhere ?
AnyWhere ?
Offline applications 5 questions to ask before creating an offline application
Question #1 What can I do offline ?
READ
CREATE
UPDATE
UPDATE, SURE ?
DELETE
Question #2 How much data is it and where can i store it ?
Few kilobytes…
Few megabytes
Hundred of megabytes
(maybe few giga)
Several gigabytes (or many more) ?
Storage Solutions
Application Cache<html manifest="/cache.manifest"> ... </html>
CACHE MANIFEST
# Explicitly cached CACHE: /favicon.ico page.html stylesheet.css images/logo.png scripts/main.js http://cdn.example.com/scripts/main.js
# Resources that require the user to be online. NETWORK: *
# static.html will be served if main.py is inaccessible # offline.jpg will be served in place of all images in images/large/ FALLBACK: /main.py /static.html images/large/ images/offline.jpg
cache.manifest
index.html
http://www.html5rocks.com/en/tutorials/appcache/beginner/http://alistapart.com/article/application-cache-is-a-douchebag
Application Cache<html manifest="/cache.manifest"> ... </html>
CACHE MANIFEST
# Explicitly cached CACHE: /favicon.ico page.html stylesheet.css images/logo.png scripts/main.js http://cdn.example.com/scripts/main.js
# Resources that require the user to be online. NETWORK: *
# static.html will be served if main.py is inaccessible # offline.jpg will be served in place of all images in images/large/ FALLBACK: /main.py /static.html images/large/ images/offline.jpg
cache.manifest
index.html
http://www.html5rocks.com/en/tutorials/appcache/beginner/http://alistapart.com/article/application-cache-is-a-douchebag
Service Workers (Cache API)
this.addEventListener('install', function(event) { event.waitUntil( caches.open('v1').then(function(cache) { return cache.addAll([ '/sw-test/', '/sw-test/index.html', '/sw-test/style.css', '/sw-test/app.js', '/sw-test/star-wars-logo.jpg', '/sw-test/gallery/', '/sw-test/gallery/myLittleVader.jpg' ]); }) ); });
2. Installation of Service Worker
if ('serviceWorker' in navigator) { navigator.serviceWorker.register(‘/sw.js') .then(function(registration) { // Registration was successful }).catch(function(err) { // registration failed :( }); }
1. Registration of Service Worker
self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { // Cache hit - return response if (response) { return response; }
var fetchRequest = event.request.clone();
return fetch(fetchRequest).then( function(response) { if (!response || response.status !== 200) { return response; }
var responseToCache = response.clone();
caches.open('v1').then(function(cache) { cache.put(event.request, responseToCache); });
return response; } ); }) ); });
3 . Fetch and Cache requests
Service Workers (Cache API)
44+40+
https://jakearchibald.github.io/isserviceworkerready/
27+
Future of upcoming web development ?
Web storage (local / session)
if (('localStorage' in window) && window['localStorage'] !== null) { localStorage.setItem(key, value); }
if (key in localStorage) { var value = localStorage.getItem(key); }
1. Store data
2. Retrieve data
if (key in localStorage) { localStorage.removeItem(key); } localStorage.clear();
3. Remove data / clear
Web SQLvar db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024); var msg;
db.transaction(function (tx) { tx.executeSql('CREATE TABLE IF NOT EXISTS LOGS (id unique, log)');
tx.executeSql('INSERT INTO LOGS (id, log) VALUES (1, "foobar")'); tx.executeSql('INSERT INTO LOGS (id, log) VALUES (2, "logmsg")'); msg = '<p>Log message created and row inserted.</p>'; document.querySelector('#status').innerHTML = msg; });
db.transaction(function (tx) { tx.executeSql('SELECT * FROM LOGS', [], function (tx, results) { var len = results.rows.length, i; msg = "<p>Found rows: " + len + "</p>"; document.querySelector('#status').innerHTML += msg; for (i = 0; i < len; i++) { msg = "<p><b>" + results.rows.item(i).log + "</b></p>";
document.querySelector('#status').innerHTML += msg; }
}, null); });
var db = openDatabase('mydb', '1.0', 'Test DB', 2 * 1024 * 1024); var msg;
db.transaction(function (tx) { tx.executeSql('CREATE TABLE IF NOT EXISTS LOGS (id unique, log)');
tx.executeSql('INSERT INTO LOGS (id, log) VALUES (1, "foobar")'); tx.executeSql('INSERT INTO LOGS (id, log) VALUES (2, "logmsg")'); msg = '<p>Log message created and row inserted.</p>'; document.querySelector('#status').innerHTML = msg; });
db.transaction(function (tx) { tx.executeSql('SELECT * FROM LOGS', [], function (tx, results) { var len = results.rows.length, i; msg = "<p>Found rows: " + len + "</p>"; document.querySelector('#status').innerHTML += msg; for (i = 0; i < len; i++) { msg = "<p><b>" + results.rows.item(i).log + "</b></p>";
document.querySelector('#status').innerHTML += msg; }
}, null); });
Web SQL
function onInitFs(fs) {
fs.root.getFile('log.txt', {}, function(fileEntry) {
// Get a File object representing the file, // then use FileReader to read its contents. fileEntry.file(function(file) { var reader = new FileReader();
reader.onloadend = function(e) { var txtArea = document.createElement('textarea'); txtArea.value = this.result; document.body.appendChild(txtArea); };
reader.readAsText(file); }, errorHandler);
}, errorHandler);
}
window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);
FileSystem API
function onInitFs(fs) {
fs.root.getFile('log.txt', {}, function(fileEntry) {
// Get a File object representing the file, // then use FileReader to read its contents. fileEntry.file(function(file) { var reader = new FileReader();
reader.onloadend = function(e) { var txtArea = document.createElement('textarea'); txtArea.value = this.result; document.body.appendChild(txtArea); };
reader.readAsText(file); }, errorHandler);
}, errorHandler);
}
window.requestFileSystem(window.TEMPORARY, 1024*1024, onInitFs, errorHandler);
FileSystem API
IndexedDBvar db;
function openDb() { var req = indexedDB.open(DB_NAME, DB_VERSION); req.onsuccess = function (evt) { db = this.result; }; req.onerror = function (evt) { console.error("openDb:", evt.target.errorCode); };
req.onupgradeneeded = function (evt) { var store = evt.currentTarget.result.createObjectStore( DB_STORE_NAME, { keyPath: 'id', autoIncrement: true });
store.createIndex('title', 'title', { unique: false }); store.createIndex('isbn', 'isbn', { unique: true }); }; }
1. Open Database
IndexedDB
var tx = db.transaction(DB_STORE_NAME, 'readwrite'); var store = tx.objectStore(DB_STORE_NAME);
var obj = { isbn: ‘0062316095’, title: ‘Sapiens: A Brief History of Humankind’, year: 2015 };
var req; try { req = store.add(obj); } catch (e) { // ... } req.onsuccess = function (evt) { console.log("Insertion in DB successful");
// ... }; req.onerror = function() { console.error("Insert error", this.error);
// ... };
2. Insert data
IndexedDBvar var tx = db.transaction(DB_STORE_NAME, 'readonly'); var store = tx.objectStore(DB_STORE_NAME);
var req = store.openCursor();
req.onsuccess = function (evt) { var cursor = evt.target.result; if (cursor) { alert(cursor.value.title); cursor.continue();
} };
3. Retrieve data (cursor)var var tx = db.transaction(DB_STORE_NAME, 'readonly'); var store = tx.objectStore(DB_STORE_NAME);
var req = store.get(42);
req.onsuccess = function (evt) { var object = evt.target.result; alert(object.title); };
3. Retrieve data (one item)
IndexedDB
var var tx = db.transaction(DB_STORE_NAME, 'readonly'); var store = tx.objectStore(DB_STORE_NAME);
var index = store.index(‘title’); var req = index.get(‘Sapiens: A Brief History of Humankind’);
req.onsuccess = function (evt) { var result = evt.target.result; if (result) { // ...
} };
3. Retrieve data (index)
IndexedDB wrappers
• db.js • joqular • TaffyDB
• localForage • IDBWrapper • YDN
IndexedDB
16+24+ 15+ 10+
8+ 4.4+
Google Gears
HTML 5 Storage Limitations
Quotas
50 %
33 %
20 %
20 %
Free disk space
Space browser can use
Space application (domain) can use
Quotas
Users
https://storage.spec.whatwg.org/ https://developers.google.com/web/updates/2016/06/persistent-storage
if (navigator.storage && navigator.storage.persist) navigator.storage.persist().then(granted => { if (granted) alert("Storage will not be cleared except by explicit user action"); else alert("Storage may be cleared by the UA under storage pressure."); });
if (navigator.storage && navigator.storage.persist) navigator.storage.persisted().then(persistent=>{ if (persistent) console.log("Storage will not be cleared except by explicit user action"); else console.log("Storage may be cleared by the UA under storage pressure."); });
Persistent storage
55+
Question #3 How to handle offline-online
synchronization ?
CONFLICTS
Basic Resolution : based on timestamp « Last version win »
Optimistic lock
Source : Patterns of Enterprise Application Architecture - Martin Fowler
System transaction boundaries
Business transaction boundaries
Pessimistic lock
Source : Patterns of Enterprise Application Architecture - Martin Fowler
System transaction boundaries
Business transaction boundaries
Theory is when you know everything but
nothing works.
Practice is when everything works but no
one knows why.
In our lab, theory and practice are combined: nothing works and no
one knows why!
kinto.jsvar db = new Kinto(); var todos = db.collection(‘todos’);
todos.create({ title: ‘buy some bread’), finished : false }) .then(function(res){…}) .catch(function(err){…})
todos.list().then(function(res) { renderTodos(res.data); }) .catch(function(err) {…});
todos.update(todo) .then(function(res) {…}) .catch(function(err) {…});
Create, Read, Update, Delete using IndexedDB
todos.delete(todo.id) .then(function(res) {…}) .catch(function(err) {…});
var syncOptions = { remote: "https://host/kintoapi", headers: {Authorization: …} }; todos.sync(syncOptions) .then(function(res){…}) .catch(function(err){…})
Synchronize with remote
kinto.jsvar syncOptions = { remote: "https://host/kintoapi", headers: {Authorization: …} }; todos.sync(syncOptions) .then(function(res){…}) .catch(function(err){…})
{ "ok": true, "lastModified": 1434617181458, "errors": [], "created": [], // created locally "updated": [], // updated locally "deleted": [], // deleted locally "published": [ // published remotely { "last_modified": 1434617181458, "done": false, "id": "7ca54d89-479a-4201-8494", "title": "buy some bread", "_status": "synced" } ], "conflicts": [], "skipped": [] }
{ "ok": true, "lastModified": 1434617181458, "errors": [], "created": [], // created locally "updated": [], // updated locally "deleted": [], // deleted locally "published": [], // published remotely "conflicts": [ { "type": "incoming", // or outgoing "local": { "last_modified": 1434619634577, "done": true, "id": "7ca54d89-479a-4201-8494", "title": "buy some bread", "_status": "updated" }, "remote": { "last_modified": 1434619745465, "done": false, "id": "7ca54d89-479a-4201-8494", "title": "buy some bread and wine" } } ], "skipped": [] }
OK Conflicts
kinto.js{ "ok": true, "lastModified": 1434617181458, "errors": [], "created": [], // created locally "updated": [], // updated locally "deleted": [], // deleted locally "published": [], // published remotely "conflicts": [ { "type": "incoming", // or outgoing "local": { "last_modified": 1434619634577, "done": true, "id": "7ca54d89-479a-4201-8494", "title": "buy some bread", "_status": "updated" }, "remote": { "last_modified": 1434619745465, "done": false, "id": "7ca54d89-479a-4201-8494", "title": "buy some bread and wine" } } ], "skipped": [] }
Conflicts
todos.sync(syncOptions) .then(function(res){
if (res.conflicts.length) { return handleConflicts(res.conflicts); }
}) .catch(function(err){…});
function handleConflicts(conflicts) { return Promise.all(conflicts.map(function(conflict) { return todos.resolve(conflict, conflict.remote); })) .then(function() { todos.sync(syncOptions); }); }
Choose your way to solve the conflict:
• Choose remote or local version • Choose according last_modified • Pick the good fields (need to provide 3-way-merge screen)
var db = new PouchDB(‘todos’);
db.post({ // can use ‘put’ with an _id title: ‘buy some bread’), finished : false }) .then(function(res){…}) .catch(function(err){…})
db.get(‘mysuperid’).then(function(todo) { // return an object with auto // generated ‘_rev’ field
// update the full doc (with _rev) todo.finished = true;
db.put(todo);
// remove the full doc (with _rev) db.remove(todo); }) .catch(function(err) {…});
Create, Read, Update, Delete using IndexedDB
var localDB = new PouchDB(‘todos’); // Remote CouchDB var remoteDB
= new PouchDB(‘http://host/todos’);
localDB.replicate.to(remoteDB); localDB.replicate.from(remoteDB); // or localDB.sync(remoteDB, { live: true, retry: true }).on('change', function (change) { // something changed! }).on('paused', function (info) { // replication was paused, // usually because of a lost connection }).on('active', function (info) { // replication was resumed }).on('error', function (err) { // unhandled error (shouldn't happen) });
Synchronize with remote
var myDoc = { _id: 'someid', _rev: '1-somerev' }; db.put(myDoc).then(function () { // success }).catch(function (err) { if (err.name === 'conflict') { // conflict! Handle it! } else { // some other error } });
Immediate conflict : error 409
_rev: ‘1-revabc’ _rev: ‘1-revabc’
_rev: ‘2-revcde’ _rev: ‘2-revjkl’
_rev: ‘1-revabc’
_rev: ‘2-revjkl’_rev: ‘2-revcde’
db.get('someid', {conflicts: true}) .then(function (doc) { // do something with the object }).catch(function (err) { // handle any errors });
{ "_id": "someid", "_rev": "2-revjkl", "_conflicts": ["2-revcde"] }
==>
Eventual conflict
==> remove the bad one, merge, … it’s up to you
Question #4
How to communicate with users ?
Inform the user …
Save Save locally
Send Send when online
… or not
Outbox (1) Send
Do no display errors !
Do not load indefinitelyyyyyyyyyy
Do not display an empty screen
Handling conflicts
Question #5 Do I really need offline ?
(2001) (2009) (2020)
« You are not on a f*cking plane and if you are, it doesn’t matter »
- David Heinemeier Hansson (2007)
https://signalvnoise.com/posts/347-youre-not-on-a-fucking-plane-and-if-you-are-it-doesnt-matter
ATAWAD
Unfortunately NOT
AnyWhere !
User Experience matters !
Thank you
Bibliography• http://diveintohtml5.info/offline.html • https://github.com/pazguille/offline-first • https://jakearchibald.com/2014/offline-cookbook/ • https://github.com/offlinefirst/research/blob/master/links.md • http://www.html5rocks.com/en/tutorials/offline/whats-offline/ • http://offlinefirst.org/ • http://fr.slideshare.net/MarcelKalveram/offline-first-the-painless-way • https://developer.mozilla.org/en-US/Apps/Fundamentals/Offline • https://uxdesign.cc/offline-93c2f8396124#.97njk8o5m • https://www.ibm.com/developerworks/community/blogs/worklight/entry/
offline_patterns?lang=en • http://apress.jensimmons.com/v5/pro-html5-programming/ch12.html • http://alistapart.com/article/offline-first • http://alistapart.com/article/application-cache-is-a-douchebag • https://logbook.hanno.co/offline-first-matters-developers-know/ • https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/
Using_Service_Workers • https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API • https://developer.chrome.com/apps/offline_storage • http://martinfowler.com/eaaCatalog/index.html • http://offlinestat.es/ • http://caniuse.com/
Jake Archibald