Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Taking the web apps offline

Taking the web apps offline

The features the modern web provides, allow us to develop web applications with rich functionality and user experience, to both - desktop and mobile users.

Although we're in the 21st century, there are still some situations when the Internet connection is not reliable. Losing connection often happens in mobile devices outside network coverage. Can we make our web application fully usable even without network connection?

In this presentation we're going to take a look at different APIs and techniques the modern web provides us to deal with such issues.

Minko Gechev

January 27, 2015
Tweet

More Decks by Minko Gechev

Other Decks in Programming

Transcript

  1. Taking the web apps offline github.com/mgechev twitter.com/mgechev Minko Gechev, January

    2015 https://www.flickr.com/photos/mandylovefly/15950777751/
  2. Motivation Although we’re in the 21st century, it often happens

    to have unreliable connection: • Entering the subway • Switching from WiFi to 3G • On a stadium with 50k more people using the same network • Saving money when using roaming • etc.
  3. What can we do about it? • Handle client-server communicational

    problems • Store the user input • Buffer user requests, when offline • Make the application available offline • Save app’s resources offline • Fallback to limited features when offline
  4. Trial and error • Make a request • Cache it

    in the fail callback • After given timeout retry the request
  5. localStorage • localStorage is HTML5 feature, which allows us to

    save data on the disk for cross-session persistence • localStorage is a collection of key-value string pairs • it provides the following simple interface: • length  -­‐  keys  count   • key(index)  -­‐  nth  key  in  the  key  list   • getItem(key)  -­‐  gets  the  value  for  given  key   • setItem(key,  value)  -­‐  sets  the  value  for  given  key   • removeItem(key)  -­‐  removes  the  value  associated  with  key
  6. localStorage sample usage form.addEventListener('submit', function (e) { if (navigator.onLine) {

    $.post(‘/api’, data); } else { var submissions = JSON.parse(localStorage.getItem(‘submissions’) || ‘[]’); submissions.push(data); localStorage.setItem(‘submissions’, JSON.stringify(submissions)); } e.preventDefault(); }, false);
  7. localStorage sample usage form.addEventListener('submit', function (e) { if (navigator.onLine) {

    $.post(‘/api’, data); } else { var submissions = JSON.parse(localStorage.getItem(‘submissions’) || ‘[]’); submissions.push(data); localStorage.setItem(‘submissions’, JSON.stringify(submissions)); } e.preventDefault(); }, false);
  8. localStorage sample usage form.addEventListener('submit', function (e) { if (navigator.onLine) {

    $.post(‘/api’, data); } else { var submissions = JSON.parse(localStorage.getItem(‘submissions’) || ‘[]’); submissions.push(data); localStorage.setItem(‘submissions’, JSON.stringify(submissions)); } e.preventDefault(); }, false);
  9. window.addEventListener('online', function () { var submissions = JSON.parse(localStorage.getItem('submissions')), promises =

    submissions.map(function (data) { return $.post('/api', data).done(function () { submissions.splice(submissions.indexOf(data), 1); }); }); $.when.apply($, promises) .done(function () { localStorage.setItem('submissions', JSON.stringify(submissions)); }) .fail(function () { localStorage.setItem('submissions', JSON.stringify(submissions)); }); }, false); app.js
  10. window.addEventListener('online', function () { var submissions = JSON.parse(localStorage.getItem('submissions')), promises =

    submissions.map(function (data) { return $.post('/api', data).done(function () { submissions.splice(submissions.indexOf(data), 1); }); }); $.when.apply($, promises) .done(function () { localStorage.setItem('submissions', JSON.stringify(submissions)); }) .fail(function () { localStorage.setItem('submissions', JSON.stringify(submissions)); }); }, false); app.js
  11. window.addEventListener('online', function () { var submissions = JSON.parse(localStorage.getItem('submissions')), promises =

    submissions.map(function (data) { return $.post('/api', data).done(function () { submissions.splice(submissions.indexOf(data), 1); }); }); $.when.apply($, promises) .done(function () { localStorage.setItem('submissions', JSON.stringify(submissions)); }) .fail(function () { localStorage.setItem('submissions', JSON.stringify(submissions)); }); }, false); app.js
  12. “The cache manifest in HTML5 is a software storage feature

    which provides the ability to access a web application even without a network connection.”
  13. Using a manifest file we describe what part of our

    application should be cached and available offline!
  14. <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Offline App</title> </head>

    <body> <img src="/images/online.png" alt=""> <script src="http://ajax.googleapis.com/ajax/libs/ jquery/1.7.2/jquery.js"></script> </body> </html> index.html
  15. <!DOCTYPE html> <html lang="en" manifest="manifest.appcache"> <head> <meta charset="UTF-8"> <title>Offline App</title>

    </head> <body> <img src="/images/online.png" alt=""> <script src="http://ajax.googleapis.com/ajax/libs/ jquery/1.7.2/jquery.js"></script> </body> </html> index.html
  16. <!DOCTYPE html> <html lang="en" manifest="manifest.appcache"> <head> <meta charset="UTF-8"> <title>Offline App</title>

    </head> <body> <img src="/images/online.png" alt=""> <script src="http://ajax.googleapis.com/ajax/libs/ jquery/1.7.2/jquery.js"></script> </body> </html> index.html
  17. CACHE  MANIFEST   CACHE:   http://ajax.googleapis.com/ajax/libs/jquery/ 1.7.2/jquery.js   FALLBACK:  

    /images/  images/fallback.jpg   NETWORK:   /api manifest.appcache
  18. Application Cache Manifest file Each manifest file • Must start

    with: “CACHE MANIFEST” • May has up to four sections: • “Explicit” • “Online whitelist” • “Fallback” • “Settings” • May has comments, prefixed with “#”
  19. CACHE MANIFEST # the above line is required # all

    comments and empty lines are ignored # starts with the explicit section, by default images/sound-icon.png images/background.png # note that each file has to be put on its own line # this is the online whitelist section NETWORK: comm.cgi # here is another set of files to cache, this time just the # CSS file. # again the explicit section CACHE: style/default.css FALLBACK: / /offline.html appcache.manifest
  20. CACHE MANIFEST # the above line is required # all

    comments and empty lines are ignored # starts with the explicit section, by default images/sound-icon.png images/background.png # note that each file has to be put on its own line # this is the online whitelist section NETWORK: comm.cgi # here is another set of files to cache, this time just the # CSS file. # again the explicit section CACHE: style/default.css FALLBACK: / /offline.html
  21. CACHE MANIFEST # the above line is required # all

    comments and empty lines are ignored # starts with the explicit section, by default images/sound-icon.png images/background.png # note that each file has to be put on its own line # this is the online whitelist section NETWORK: comm.cgi # here is another set of files to cache, this time just the # CSS file. # again the explicit section CACHE: style/default.css FALLBACK: / /offline.html
  22. CACHE MANIFEST # the above line is required # all

    comments and empty lines are ignored # starts with the explicit section, by default images/sound-icon.png images/background.png # note that each file has to be put on its own line # this is the online whitelist section NETWORK: comm.cgi # here is another set of files to cache, this time just the # CSS file. # again the explicit section CACHE: style/default.css FALLBACK: / /offline.html appcache.manifest
  23. CACHE MANIFEST # the above line is required # all

    comments and empty lines are ignored # starts with the explicit section, by default images/sound-icon.png images/background.png # note that each file has to be put on its own line # this is the online whitelist section NETWORK: comm.cgi # here is another set of files to cache, this time just the # CSS file. # again the explicit section CACHE: style/default.css FALLBACK: / /offline.html appcache.manifest
  24. CACHE MANIFEST # the above line is required # all

    comments and empty lines are ignored # starts with the explicit section, by default images/sound-icon.png images/background.png # note that each file has to be put on its own line # this is the online whitelist section NETWORK: comm.cgi # here is another set of files to cache, this time just the # CSS file. # again the explicit section CACHE: style/default.css FALLBACK: / /offline.html appcache.manifest
  25. • If find a manifest attribute • checking   •

    noupdate - if the manifest file hasn’t been updated • downloading - downloading cached resources • progress - resource downloaded • error - error encountered • updateready - update was successfully completed • cached - fired after the initial caching has been completed • obsolete  - fired when the manifest has been removed Application Cache Events
  26. Finds manifest Render Update cache Cached Changed Load Render Cache

    High-level overview with events (incomplete) downloading checking cached noupdate downloading updateready progress/error progress/ error
  27. The online whitelist section lists resources that should be never

    cached We can use the “online whitelist wildcard flag” - *
  28. // swaps the current content of the cached // with

    the content of the temporal cache applicationCache.swapCache(); // check for updates programmatically // requires the manifest to be changed applicationCache.update(); // abort's the update process applicationCache.abort(); app.js
  29. HTTP/1.1  200  OK   Server:  nginx/1.6.2   Date:  Tue,  13

     Jan  2015  14:26:33  GMT   Content-­‐Type:  text/cache-­‐manifest   Content-­‐Length:  2088   Expires:  Sat,  01  Dec  3094  16:00:00  GMT   CACHE  MANIFEST   NETWORK:
 *   IMAGES:   /images   FALLBACK:   /  /fallback.html
  30. –Jake Archibald “ServiceWorker is a background worker, it gives us

    a JavaScript context to add features such as push messaging, background sync, geofencing and network control.”
  31. –Jake Archibald “ServiceWorker is a background worker, it gives us

    a JavaScript context to add features such as push messaging, background sync, geofencing and network control.”
  32. 'use strict'; if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js', { scope:

    '/sample-scope' }) .then(function (sw) { console.log('Successfully registered', sw); }, function () { console.error('Error'); }); } app.js
  33. 'use strict'; if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js', { scope:

    '/sample-scope' }) .then(function (sw) { console.log('Successfully registered', sw); }, function () { console.error('Error'); }); } app.js
  34. 'use strict'; if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js', { scope:

    '/sample-scope' }) .then(function (sw) { console.log('Successfully registered', sw); }, function () { console.error('Error'); }); } app.js
  35. 'use strict'; if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js', { scope:

    '/sample-scope' }) .then(function (sw) { console.log('Successfully registered', sw); }, function () { console.error('Error'); }); } Promise app.js
  36. Asynchronously traversing the DOM tree Each node: • could be

    a promise or DOM element • could has child nodes function traverse(node) { return Q.all([].slice.call(node.children).map(function (c) { return Q.when(c); })) .then(function (nodes) { return Q.all(nodes.map(traverse)) .then(function (els) { return els.reduce(function (prev, arr) { return arr.concat(prev); }, []).concat(nodes); }); }); }
  37. ServiceWorkers • Running in a different thread • Not blocking

    the main execution thread • Located in the same origin • Prevents XSS attacks • Only via HTTPS • Prevents MITM attacks • Completely asynchronous • No synchronous APIs (like localStorage) • Can be alive even with closed browser! • Allow background synchronization • Allow push notifications
  38. self.addEventListener('install', function (e) { e.waitUntil( caches.open(CACHE_NAME) .then(function (cache) { var

    requests = URLs.map(function (url) { return new Request(url); }); return Promise.all(requests.map(function (r) { return fetch(r); }).then(function (vals) { return vals.forEach(function (item, idx) { cache.put(requests[idx], item); }); })); }) ); }); sw.js
  39. self.addEventListener('install', function (e) { e.waitUntil( caches.open(CACHE_NAME) .then(function (cache) { var

    requests = URLs.map(function (url) { return new Request(url); }); return Promise.all(requests.map(function (r) { return fetch(r); }).then(function (vals) { return vals.forEach(function (item, idx) { cache.put(requests[idx], item); }); })); }) ); }); sw.js
  40. self.addEventListener('install', function (e) { e.waitUntil( caches.open(CACHE_NAME) .then(function (cache) { var

    requests = URLs.map(function (url) { return new Request(url); }); return Promise.all(requests.map(function (r) { return fetch(r); }).then(function (vals) { return vals.forEach(function (item, idx) { cache.put(requests[idx], item); }); })); }) ); }); sw.js
  41. self.addEventListener('install', function (e) { e.waitUntil( caches.open(CACHE_NAME) .then(function (cache) { var

    requests = URLs.map(function (url) { return new Request(url); }); return Promise.all(requests.map(function (r) { return fetch(r); }).then(function (vals) { return vals.forEach(function (item, idx) { cache.put(requests[idx], item); }); })); }) ); }); sw.js
  42. self.addEventListener('install', function (e) { e.waitUntil( caches.open(CACHE_NAME) .then(function (cache) { var

    requests = URLs.map(function (url) { return new Request(url); }); return Promise.all(requests.map(function (r) { return fetch(r); }).then(function (vals) { return vals.forEach(function (item, idx) { cache.put(requests[idx], item); }); })); }) ); }); sw.js
  43. self.addEventListener('fetch', function (event) { event.respondWith( caches.match(event.request).then(function (response) { if (response)

    { console.log('Found response in cache'); return response; } console.log('No response found in cache.'); return fetch(event.request).then( function (response) { console.log('Response from network is'); return response; }).catch(function (error) { console.error('Fetching failed:', error); }); }) ); }); sw.js
  44. self.addEventListener('fetch', function (event) { event.respondWith( caches.match(event.request).then(function (response) { if (response)

    { console.log('Found response in cache'); return response; } console.log('No response found in cache.'); return fetch(event.request).then( function (response) { console.log('Response from network is'); return response; }).catch(function (error) { console.error('Fetching failed:', error); }); }) ); }); sw.js
  45. self.addEventListener('fetch', function (event) { event.respondWith( caches.match(event.request).then(function (response) { if (response)

    { console.log('Found response in cache'); return response; } console.log('No response found in cache.'); return fetch(event.request).then( function (response) { console.log('Response from network is'); return response; }).catch(function (error) { console.error('Fetching failed:', error); }); }) ); }); sw.js
  46. sw.js self.addEventListener('fetch', function (event) { event.respondWith( caches.match(event.request).then(function (response) { if

    (response) { console.log('Found response in cache'); return response; } console.log('No response found in cache.'); return fetch(event.request).then( function (response) { console.log('Response from network is'); return response; }).catch(function (error) { console.error('Fetching failed:', error); }); }) ); });
  47. • Cache only • When the app is offline •

    Network only • Newest resources possible • Cache, falling back to network • For offline-first applications • Cache & network race • Devices with slow disk access • Cache then network • For content that updates frequently • Generic fallback • When we have unavailable service Sample strategies