Slide 1

Slide 1 text

Taking the web apps offline github.com/mgechev twitter.com/mgechev Minko Gechev, January 2015 https://www.flickr.com/photos/mandylovefly/15950777751/

Slide 2

Slide 2 text

Agenda • Introduction • Connection Events • Local Storage • Application Cache • Service Workers

Slide 3

Slide 3 text

Motivation

Slide 4

Slide 4 text

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.

Slide 5

Slide 5 text

How to make our app works offline?

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

But how do we know whether we’re online?

Slide 8

Slide 8 text

Trial and error • Make a request • Cache it in the fail callback • After given timeout retry the request

Slide 9

Slide 9 text

Is that all can do?

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

window.addEventListener('offline', function (e) { alert('offline'); }, false); window.addEventListener('online', function (e) { alert('online'); }, false); events.js

Slide 12

Slide 12 text

How do we know if we’re already offline?

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Caching the user input

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

localStorage vs sessionStorage vs cookies

Slide 17

Slide 17 text

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);

Slide 18

Slide 18 text

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);

Slide 19

Slide 19 text

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);

Slide 20

Slide 20 text

…and when the user gets online…

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Under the hood…

Slide 25

Slide 25 text

Note that the XMLHttpRequest may also fail

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

We cache the user input but what about our UI?

Slide 28

Slide 28 text

Application Cache

Slide 29

Slide 29 text

One of the most controversial HTML5 features

Slide 30

Slide 30 text

“The cache manifest in HTML5 is a software storage feature which provides the ability to access a web application even without a network connection.”

Slide 31

Slide 31 text

Using a manifest file we describe what part of our application should be cached and available offline!

Slide 32

Slide 32 text

Offline App index.html

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

Offline App index.html

Slide 35

Slide 35 text

Offline App index.html

Slide 36

Slide 36 text

CACHE  MANIFEST   CACHE:   http://ajax.googleapis.com/ajax/libs/jquery/ 1.7.2/jquery.js   FALLBACK:   /images/  images/fallback.jpg   NETWORK:   /api manifest.appcache

Slide 37

Slide 37 text

No content

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

No content

Slide 41

Slide 41 text

Before criticizing, lets reveal the magic

Slide 42

Slide 42 text

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 “#”

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Finds manifest Render Update cache Cached Changed Load Render Cache High-level overview (incomplete)

Slide 50

Slide 50 text

• 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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

The online whitelist section lists resources that should be never cached We can use the “online whitelist wildcard flag” - *

Slide 53

Slide 53 text

What if we miss the network section…?

Slide 54

Slide 54 text

No content

Slide 55

Slide 55 text

No content

Slide 56

Slide 56 text

No content

Slide 57

Slide 57 text

Which translated to English means…

Slide 58

Slide 58 text

DO NOT FORGET THE *

Slide 59

Slide 59 text

Is there anything tricky in the fallback section?

Slide 60

Slide 60 text

No content

Slide 61

Slide 61 text

No network connection With network connection

Slide 62

Slide 62 text

And wait for it…

Slide 63

Slide 63 text

AppCache also has a JavaScript API

Slide 64

Slide 64 text

// 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

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

more like…

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

Development tools

Slide 69

Slide 69 text

chrome://appcache-internals DevTools

Slide 70

Slide 70 text

And one more thing…

Slide 71

Slide 71 text

AppCache is another layer of caching

Slide 72

Slide 72 text

No content

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

No content

Slide 75

Slide 75 text

No content

Slide 76

Slide 76 text

Service Workers

Slide 77

Slide 77 text

–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.”

Slide 78

Slide 78 text

–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.”

Slide 79

Slide 79 text

Lets think of the ServiceWorkers as HTTP proxies on the client

Slide 80

Slide 80 text

IsServiceWorkerReadyYet.com

Slide 81

Slide 81 text

We won’t drive into details because the spec is still in development

Slide 82

Slide 82 text

How to add a service worker without breaking the web?

Slide 83

Slide 83 text

'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

Slide 84

Slide 84 text

'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

Slide 85

Slide 85 text

For now, we can think of the ServiceWorker more like an enhancement

Slide 86

Slide 86 text

'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

Slide 87

Slide 87 text

'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

Slide 88

Slide 88 text

ECMAScript 6 comes with built-in Promises

Slide 89

Slide 89 text

No content

Slide 90

Slide 90 text

Promises can be used instead of callbacks for handling asynchronous calls

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

we can use them for… other things…. For fun & profit

Slide 94

Slide 94 text

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); }); }); }

Slide 95

Slide 95 text

Exclusively used by the Service Workers

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

Install event

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

Fetch event

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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); }); }) ); });

Slide 108

Slide 108 text

Loading strategies

Slide 109

Slide 109 text

• 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

Slide 110

Slide 110 text

https://speakerdeck.com/jaffathecake/in-your-font-face

Slide 111

Slide 111 text

We can debug them!

Slide 112

Slide 112 text

No content

Slide 113

Slide 113 text

Thank you! github.com/mgechev twitter.com/mgechev