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.

82bafb0432ce4ccc9dcc26f94d5fe5bc?s=128

Minko Gechev

January 27, 2015
Tweet

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. Agenda • Introduction • Connection Events • Local Storage •

    Application Cache • Service Workers
  3. Motivation

  4. 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.
  5. How to make our app works offline?

  6. 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
  7. But how do we know whether we’re online?

  8. Trial and error • Make a request • Cache it

    in the fail callback • After given timeout retry the request
  9. Is that all can do?

  10. None
  11. window.addEventListener('offline', function (e) { alert('offline'); }, false); window.addEventListener('online', function (e)

    { alert('online'); }, false); events.js
  12. How do we know if we’re already offline?

  13. if (!navigator.onLine) { // you're offline m8 } app.js

  14. Caching the user input

  15. 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
  16. localStorage vs sessionStorage vs cookies

  17. 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);
  18. 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);
  19. 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);
  20. …and when the user gets online…

  21. 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
  22. 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
  23. 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
  24. Under the hood…

  25. Note that the XMLHttpRequest may also fail

  26. None
  27. We cache the user input but what about our UI?

  28. Application Cache

  29. One of the most controversial HTML5 features

  30. “The cache manifest in HTML5 is a software storage feature

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

    application should be cached and available offline!
  32. <!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
  33. None
  34. <!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
  35. <!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
  36. CACHE  MANIFEST   CACHE:   http://ajax.googleapis.com/ajax/libs/jquery/ 1.7.2/jquery.js   FALLBACK:  

    /images/  images/fallback.jpg   NETWORK:   /api manifest.appcache
  37. None
  38. None
  39. None
  40. None
  41. Before criticizing, lets reveal the magic

  42. 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 “#”
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. Finds manifest Render Update cache Cached Changed Load Render Cache

    High-level overview (incomplete)
  50. • 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
  51. 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
  52. The online whitelist section lists resources that should be never

    cached We can use the “online whitelist wildcard flag” - *
  53. What if we miss the network section…?

  54. None
  55. None
  56. None
  57. Which translated to English means…

  58. DO NOT FORGET THE *

  59. Is there anything tricky in the fallback section?

  60. None
  61. No network connection With network connection

  62. And wait for it…

  63. AppCache also has a JavaScript API

  64. // 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
  65. None
  66. more like…

  67. https://speakerdeck.com/jaffathecake/application-cache-douchebag

  68. Development tools

  69. chrome://appcache-internals DevTools

  70. And one more thing…

  71. AppCache is another layer of caching

  72. None
  73. 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
  74. None
  75. None
  76. Service Workers

  77. –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.”
  78. –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.”
  79. Lets think of the ServiceWorkers as HTTP proxies on the

    client
  80. IsServiceWorkerReadyYet.com

  81. We won’t drive into details because the spec is still

    in development
  82. How to add a service worker without breaking the web?

  83. '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
  84. '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
  85. For now, we can think of the ServiceWorker more like

    an enhancement
  86. '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
  87. '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
  88. ECMAScript 6 comes with built-in Promises

  89. None
  90. Promises can be used instead of callbacks for handling asynchronous

    calls
  91. getJSON('/data.json') .then(function (data) { console.log(data); }, function (error) { console.log(error);

    }); promise.js
  92. promises.js Promise.all([ getJSON('/a.json'), getJSON('/b.json'), getJSON('/c.json') ]).then(function (res) { console.log( res[0],

    res[1], res[2] ); });
  93. we can use them for… other things…. For fun &

    profit
  94. 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); }); }); }
  95. Exclusively used by the Service Workers

  96. 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
  97. Install event

  98. 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
  99. 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
  100. 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
  101. 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
  102. 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
  103. Fetch event

  104. 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
  105. 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
  106. 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
  107. 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); }); }) ); });
  108. Loading strategies

  109. • 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
  110. https://speakerdeck.com/jaffathecake/in-your-font-face

  111. We can debug them!

  112. None
  113. Thank you! github.com/mgechev twitter.com/mgechev