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

Pragmatist's Guide to Service Worker: Smashing NYC 2017

Pragmatist's Guide to Service Worker: Smashing NYC 2017

Service Workers are buzzy and exciting, a collection of Web APIs that do all sorts of handy stuff. Yet you’re a busy web dev. You’ve got sites to build, web apps to code. Let’s decode the complexities of Service Worker capabilities, breaking them down into concepts, recipes and examples.

We’ll get you ready to build real-life useful stuff without the need for an overwhelming or distracting investment of time and energy.

Lyza Gardner

June 20, 2017
Tweet

More Decks by Lyza Gardner

Other Decks in Technology

Transcript

  1. Pragmatist’s Guide to Service Worker I am Lyza Danger Gardner

    I am an Open Web Engineer at Bocoup I can be found at @lyzadanger and https://www.lyza.com Smashing Conference NYC 2017
  2. Hi! Service Worker

  3. A Service Worker is a script…

  4. A Service Worker consists of a script.

  5. A Service Worker can act as a proxy.

  6. without service worker…

  7. Client (Browser) Network index.html ? index.html ! browser requests resources

    from network…
  8. Client Network

  9. Network Client

  10. Network Client Service Worker

  11. Client Network

  12. • Consists of a script (JavaScript file) • Acts as

    a proxy between client (browser) and network A Service Worker: …and quite a lot more. • It can: • define and handle offline experiences • improve online web performance…
  13. Who is this presentation for?

  14. (Yep. There will be JavaScript.)

  15. But there will also be wretchedly-drawn characters!

  16. http://bit.ly/sw-smashing-nyc

  17. • Consists of a script (JavaScript file) • Acts as

    a proxy between client (browser) and network • It can: • define and handle offline experiences • improve online web performance… A Service Worker: …and quite a lot more. with regards to this…
  18. Let’s Give it Context.

  19. Notifications Background Sync Channel Messaging An ecosystem of related APIs

  20. WebWorker

  21. • An object created from within a context • Consists

    of a JavaScript file • The code in that JavaScript file is run in a worker thread • A web worker has a different global context A Web Worker:
  22. • An object created from within a context • Consists

    of a JavaScript file • The code in that JavaScript file is run in a worker thread • A web worker has a different global context A Web Worker: ??? ???
  23. worker browsing context e.g. a browser window showing a document

    e.g. a ServiceWorker
  24. ServiceWorkerGlobalScope window (worker) thread (execution) thread

  25. ServiceWorkerGlobalScope window (worker) thread (execution) thread Channel Messaging, e.g.

  26. ServiceWorkerGlobalScope window

  27. This is starting to seem complicated.

  28. Eeeek! Power and flexibility involves some complexity.

  29. Service worker is pretty chill.

  30. async only all HTTPS A couple of across-the-board rules.

  31. On its way https://jakearchibald.github.io/isserviceworkerready/ …

  32. None
  33. Let’s Do Something Basic.

  34. Let’s Make a Service Worker that Accomplishes Nothing.

  35. We’ll create a JavaScript file for a Service Worker that

    will listen for fetch events and…do nothing for now.
  36. listen for fetch events fetch

  37. Service Worker Browser Fetch Event Network listen for fetch events

  38. self.addEventListener('fetch', event => { // Do something about it });

    service-worker.js Adding an event listener for ‘fetch’ events
  39. That’s seriously it for the moment…

  40. Registering our Useless Service Worker

  41. We need to add some JavaScript to a web page

    to register the Service Worker. <script> //… </script> index.html service-worker.js
  42. A Service Worker is registered against a scope.

  43. Is a path or a pattern, relative to the service

    worker script’s location, you know, e.g.: ./ ./foo ./foo/bar/baz Scope:
  44. I want to register the service worker located at service-worker.js

    against the scope ./blog index.html <script> //… </script>
  45. I want to register the service worker located at service-worker.js

    against the scope ./blog index.html blog service-worker.js
  46. <!doctype html> <html> <head> <!-- ... --> </head> <body> <script>

    if ('serviceWorker' in navigator) { navigator.serviceWorker.register('service-worker.js'); } </script> </body> </html> index.html Register a service worker
  47. if ('serviceWorker' in navigator) { navigator.serviceWorker.register('service-worker.js'); } index.html detail Feature

    detection for service worker support
  48. navigator.serviceWorker …provides an object…including facilities to register, unregister and update

    service workers, and access the state of service workers and their registrations. Sincerely, The Service Worker Specification [ServiceWorkerContainer]
  49. .register( ) navigator.serviceWorker scriptURL,options

  50. if ('serviceWorker' in navigator) { navigator.serviceWorker.register('service-worker.js'); } index.html detail

  51. Wait a minute: what happened to scope?

  52. navigator.serviceWorker.register(scriptURL, options) Scope (String) can be passed as second argument

  53. if ('serviceWorker' in navigator) { navigator.serviceWorker.register('service-worker.js', './'); } code example

  54. if ('serviceWorker' in navigator) { navigator.serviceWorker.register('service-worker.js'); } index.html detail Default

    scope is ‘./’
  55. if ('serviceWorker' in navigator) { navigator.serviceWorker.register('service-worker.js'); } self.addEventListener('fetch', event =>

    { // Do something about it }); index.html detail service-worker.js
  56. Let’s enhance the uselessness.

  57. self.addEventListener('fetch', event => { // Do something about it });

    service-worker.js
  58. FetchEvent

  59. Things in FetchEvent…

  60. request respondWith(…) FetchEvent object

  61. Respond with…with…? self.addEventListener('fetch', event => { event.respondWith( ); }); FetchEvent.respondWith(…)

    service-worker.js
  62. The fetch API

  63. Request Response

  64. fetch( ) Request Response Network fetch a request, then (we

    hope) get a response
  65. Response FetchEvent.respondWith( )

  66. service-worker.js FetchEvent self.addEventListener('fetch', event => { });

  67. service-worker.js self.addEventListener('fetch', event => { event.respondWith( ); }); FetchEvent.respondWith()

  68. fetch( ) Request Response

  69. service-worker.js self.addEventListener('fetch', event => { event.respondWith( fetch(event.request) ); }); FetchEvent.request

    respondWith the Promise returned by fetch
  70. Service Worker Browser Fetch Event Network I need ‘./foo.gif’

  71. JavaScript Promises in 60 seconds

  72. Promise

  73. = fetch( ) Promise Request

  74. Fulfilled Pending

  75. Rejected Pending

  76. Pending OR Settled: Fulfilled Settled: Rejected

  77. None
  78. A Service Worker that Does Something!

  79. We’ll extend our Service Worker so that it shows a

    custom offline message to a user when they are offline.
  80. Client Network Online

  81. Client Network Online

  82. <!doctype html> <html> <head> </head> <body> <p>If you see this,

    you are online</p> <script> if ('serviceWorker' in navigator) { navigator.serviceWorker.register('service-worker.js', './'); } </script> </body> </html> index.html
  83. Network Client Service Worker Offline

  84. Network Client Service Worker Offline

  85. self.addEventListener('fetch', event => { event.respondWith( fetch(event.request) ); }); service-worker.js Network

    The Happy (Online) Path
  86. fetch( ) Promise Request Response Network The Happy (Online) Path

  87. fetch( ) Request Offline! Network The Sad (Offline) Path

  88. fetch( ) Request Offline! Network .catch( ) The Sad (Offline)

    Path
  89. event.respondWith( fetch(event.request).catch(error => { // Do something about it })

    ); service-worker.js (detail) handle the offline situation in the catch
  90. Response need a web-page-like Response!

  91. new Response(body, init)

  92. event.respondWith( fetch(event.request).catch(error => { return new Response( ); }) );

    service-worker.js (detail)
  93. event.respondWith( fetch(event.request).catch(error => { return new Response('<p>Oh, dear.</p>’, ); })

    ); service-worker.js (detail)
  94. service-worker.js (detail) event.respondWith( fetch(event.request).catch(error => { return new Response('<p>Oh, dear.</p>',

    { headers: { 'Content-Type': 'text/html' } }); }) );
  95. There’s a problem with this.

  96. Offline: we’re responding to all fetches with a janky web

    page.
  97. event.request.mode

  98. event.request.mode === ‘navigate’

  99. event.request.method === 'GET' && In browsers that don’t support request.mode

  100. event.request.method === 'GET' && event.request.headers.get(‘accept').includes('text/html')) In browsers that don’t support

    request.mode
  101. self.addEventListener('fetch', event => { if (event.request.mode === 'navigate') { event.respondWith(

    fetch(request).catch(error => { return new Response('<p>Oh, dear.</p>', { headers: { 'Content-Type': 'text/html' } }); }) ); } }); service-worker.js
  102. index.html service-worker.js self.addEventListener('fetch', event => { if (event.request.mode === 'navigate')

    { event.respondWith( fetch(request).catch(error => { return new Response('<p>Oh, dear.</p>', { headers: { 'Content-Type': 'text/html' } }); }) ); } }); <!doctype html> <html> <head> </head> <body> <p>If you see this, you are online</p> <script> if ('serviceWorker' in navigator) { navigator.serviceWorker.register('service-worker.js', './'); } </script> </body> </html>
  103. Now we have an offline message.

  104. We can do better.

  105. Let’s use our SW to serve an offline page.

  106. self.addEventListener('fetch', event => { if (event.request.mode === 'navigate') { event.respondWith(

    fetch(request).catch(error => { return new Response('<p>Oh, dear.</p>', { headers: { 'Content-Type': 'text/html' } }); }) ); } }); service-worker.js Let’s not do this…
  107. <!doctype html> <html> <head> <meta charset="utf-8"> <title>Registering a Service Worker</title>

    <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="/default.css" rel="stylesheet" title="Default Style"> </head> <body> <h1>Offline Fallback Test</h1> <p class="error">If you see this, you are offline</p> </body> </html> offline.html instead, let’s use a real HTML page
  108. We’ll extend our Service Worker so that it provides a

    custom offline page when navigation requests are made and the network is unavailable.
  109. <!doctype html> <html> <head> <meta charset="utf-8"> <title>Registering a Service Worker</title>

    <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="/default.css" rel="stylesheet" title="Default Style"> </head> <body> <h1>Offline Fallback Test</h1> <p class="error">If you see this, you are offline</p> </body> </html> offline.html Gotta stash the offline page first!
  110. Service Worker lifecycle

  111. parsed Service Worker state

  112. parsed installing Service Worker state

  113. parsed installing installed Service Worker state

  114. install phase

  115. self.addEventListener('install', event => { // Stash offline page in a

    cache }); service-worker.js (detail)
  116. Install Event

  117. ExtendableEvent

  118. ExtendableEvent.waitUntil(promise)

  119. self.addEventListener('install', event => { event.waitUntil( // ... ); }); service-worker.js

    (detail)
  120. The Cache API

  121. cache

  122. cache

  123. cache

  124. cache offline-items

  125. cache offline-items

  126. You have complete control over caches

  127. CacheStorage caches

  128. methods ServiceWorkerGlobalScope properties fetch caches

  129. Caching Offline Page

  130. .open(‘offline’) caches

  131. .open(‘offline’) caches

  132. new Request( ) ‘offline.html’ offline need both a Request and

    Response to put in cache…
  133. new Response(body, init) fetch We want the Response to represent

    the offline.html page…
  134. self.addEventListener('install', event => { var offlineURL = 'offline.html'; event.waitUntil( );

    }); service-worker.js (install event handler detail)
  135. self.addEventListener('install', event => { var offlineURL = 'offline.html'; event.waitUntil( );

    }); service-worker.js (install event handler detail) service-worker.js (install event handler detail)
  136. self.addEventListener('install', event => { var offlineURL = 'offline.html'; event.waitUntil( fetch(new

    Request(offlineURL)).then(response => { }) ); }); Step 1 service-worker.js (install event handler detail)
  137. self.addEventListener('install', event => { var offlineURL = 'offline.html'; event.waitUntil( fetch(new

    Request(offlineURL)).then(response => { return caches.open('offline').then(cache => { }); }) ); }); Step 2 service-worker.js (install event handler detail)
  138. cache.put(request, response)

  139. self.addEventListener('install', event => { var offlineURL = 'offline.html'; event.waitUntil( fetch(new

    Request(offlineURL)).then(response => { return caches.open('offline').then(cache => { return cache.put(new Request(offlineURL), response); }); }) ); }); Step 3 service-worker.js (install event handler detail)
  140. offline caches.open( ) fetch( ) cache.put( , ) ‘offline.html’ offline

    Step 1 Step 2 Step 3
  141. Serving the offline page.

  142. self.addEventListener('fetch', event => { if (event.request.mode === 'navigate') { event.respondWith(

    fetch(request).catch(error => { return new Response('<p>Oh, dear.</p>', { headers: { 'Content-Type': 'text/html' } }); }) ); } }); service-worker.js (fetch event handler detail) instead of…
  143. cache.match(request) find the offline page in cache…

  144. self.addEventListener('fetch', event => { if (event.request.mode === 'navigate') { event.respondWith(

    fetch(event.request).catch(error => { return caches.open('offline').then(cache => { }); }) ); } }); service-worker.js (fetch event handler detail)
  145. self.addEventListener('fetch', event => { if (event.request.mode === 'navigate') { event.respondWith(

    fetch(event.request).catch(error => { return caches.open('offline').then(cache => { return cache.match('offline.html'); }); }) ); } }); service-worker.js (fetch event handler detail) You can use URL in place of Request here…
  146. return cache.match(new Request('offline.html')); return cache.match('offline.html'); is same effect as:

  147. Client Network Online

  148. Network Client Service Worker Offline

  149. Network Strategies

  150. We can make a Service Worker that implements different network

    strategies for different types of requests.
  151. HTML network caches 1 2 network-first strategy

  152. image network caches 1 2 cache-first strategy

  153. 1 2 Determining what kind of request a given fetch

    represents. Getting stuff into caches so it’s there for later.
  154. self.addEventListener('fetch', event => { if (event.request.mode === 'navigate') { event.respondWith(/*

    network-first strategy */); } }); 1 service-worker.js (fetch handler detail)
  155. self.addEventListener('fetch', event => { if (event.request.mode === 'navigate') { event.respondWith(/*

    network-first strategy */); } else if (event.request.headers.get('Accept').indexOf('image') !== -1) { event.respondWith(/* cache-first strategy */); } }); 1 service-worker.js (fetch handler detail)
  156. 1 2 Determining what kind of request a given fetch

    represents. Getting stuff into caches so it’s there for later.
  157. Network-First Strategy

  158. Part I: Online Behavior (a.k.a. the “happy path”)

  159. assets caches.open( ) fetch( ) cache.put( , ) assets network-first

    strategy Step 1 Step 2 Step 3 Network
  160. Read-through caching 2 if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response

    => { }) ); } service-worker.js (fetch handler detail)
  161. Response.ok if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => {

    if (response.ok) { } }) ); } service-worker.js (fetch handler detail) 2
  162. Response.clone() if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => {

    if (response.ok) { caches.open('assets').then(cache => { cache.put(event.request, response.clone()); }); } }) ); } service-worker.js (fetch handler detail) 2
  163. 2 if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => {

    if (response.ok) { caches.open('assets').then(cache => { cache.put(event.request, response.clone()); }); } return response; }) ); } service-worker.js (fetch handler detail)
  164. Part II: Offline Behavior (a.k.a. the fallback path)

  165. caches.match( ) fetch( ) network-first strategy Network

  166. caches.match( )

  167. vs. undefined caches.match( ) cache.match( ) when no match found,

    Promise resolves to undefined
  168. if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => { if

    (response.ok) { caches.open('assets').then(cache => { cache.put(event.request, response.clone()); }); } return response; }) ); } service-worker.js (fetch handler detail)
  169. if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => { })

    ); } service-worker.js (fetch handler detail) fold happy path out of the way…
  170. if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => { })

    .catch(() => { }) ); } service-worker.js (fetch handler detail)
  171. if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => { })

    .catch(() => { return caches.match(event.request).then(response => { }); }) ); } service-worker.js (fetch handler detail)
  172. if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => { })

    .catch(() => { return caches.match(event.request).then(response => { if (!response) { throw Error(`${request.url} not found in cache`); } }); }) ); } service-worker.js (fetch handler detail)
  173. if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => { })

    .catch(() => { return caches.match(event.request).then(response => { if (!response) { throw Error(`${request.url} not found in cache`); } return response; }); }) ); } service-worker.js (fetch handler detail)
  174. Part IIa: Offline Behavior (a.k.a. the fallback fallback path)

  175. caches.match( ) fetch( ) cache.match( ) offline caches.open(‘offline’) ‘offline.html’

  176. if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => { })

    .catch(() => { return caches.match(event.request).then(response => { if (!response) { throw Error(`${request.url} not found in cache`); } return response; }); }) ); } service-worker.js (fetch handler detail) service-worker.js (fetch handler detail)
  177. if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => { })

    .catch(() => { }) ); } service-worker.js (fetch handler detail) service-worker.js (fetch handler detail)
  178. if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => { })

    .catch(() => { }) .catch(() => { }) ); } service-worker.js (fetch handler detail) service-worker.js (fetch handler detail)
  179. if (event.request.mode === 'navigate') { event.respondWith(fetch(event.request) .then(response => { })

    .catch(() => { }) .catch(() => { return caches.open('offline').then(cache => { return cache.match('offline.html'); }); }) ); } service-worker.js (fetch handler detail) service-worker.js (fetch handler detail)
  180. caches.match( ) fetch( ) network-first strategy

  181. caches.match( ) fetch( ) cache-first strategy undefined

  182. assets caches.open( ) fetch( ) cache.put( , ) assets read-through

    caching
  183. cache.put(request, response) cache.add(requestOrURL) “ ”

  184. cache.addAll(urls) “ ” cache.add(requestOrURL)

  185. Application Shell

  186. Need to precache app shell assets during install…

  187. 1 2 Put assets into cache during install phase. Later,

    respond to fetches for static assets with a cache-first strategy.
  188. const cacheFiles = [ '', 'default.css', 'static-assets/cloud-1.jpg', 'static-assets/cloud-2.jpg', 'static-assets/cloud-3.jpg', 'static-assets/cloud-4.jpg',

    'static-assets/cloud-5.jpg', 'static-assets/cloud-6.jpg', 'static-assets/cloud-7.jpg', 'static-assets/cloud-8.jpg', 'static-assets/cloud-9.jpg', 'static-assets/cloud-10.jpg' ]; service-worker.js (detail) 1
  189. self.addEventListener('install', event => { event.waitUntil( caches.open('static').then(cache => { }) );

    }); service-worker.js (install handler detail) 1
  190. self.addEventListener('install', event => { event.waitUntil( caches.open('static').then(cache => { return cache.addAll(cacheFiles);

    }) ); }); 1 service-worker.js (install handler detail)
  191. self.addEventListener('fetch', event => { var url = new URL(event.request.url); if

    (cacheFiles.indexOf(url.pathname) !== -1) { event.respondWith( ); } }); 2 service-worker.js (fetch handler detail) responding to fetch events for app shell assets
  192. self.addEventListener('fetch', event => { var url = new URL(event.request.url); if

    (cacheFiles.indexOf(url.pathname) !== -1) { event.respondWith( caches.match(event.request).then(response => { if (!response) { throw Error(`${event.request.url} not found in cache`); } return response; }) ); } }); 2 service-worker.js (fetch handler detail)
  193. self.addEventListener('fetch', event => { var url = new URL(event.request.url); if

    (cacheFiles.indexOf(url.pathname) !== -1) { event.respondWith( caches.match(event.request).then(response => { if (!response) { throw Error(`${event.request.url} not found in cache`); } return response; }).catch(() => fetch(event.request)) ); } }); 2 service-worker.js (fetch handler detail)
  194. Service Worker state parsed installing installed

  195. parsed installing installed Service Worker state activating

  196. parsed installing installed Service Worker state activating activated

  197. service-worker.js (NEW!) parsed installing installed activating activated

  198. service-worker.js (NEW!) parsed installing installed activating activated

  199. service-worker.js (NEW!) parsed installing installed activating activated

  200. service-worker.js (NEW!) parsed installing installed activating activated

  201. service-worker.js (NEW!) parsed installing installed activating activated

  202. parsed installing installed activating activated service-worker.js (NEW!) ?

  203. parsed installing installed activating activated service-worker.js (NEW!) X If no

    previous SW, activation starts automatically
  204. parsed installing installed activating activated service-worker.js (NEW!) X

  205. parsed installing installed activating activated service-worker.js (NEW!) X

  206. parsed installing installed activating activated service-worker.js (NEW!)

  207. parsed installing installed activating activated service-worker.js (NEW!) ?

  208. parsed installing installed activating activated service-worker.js (NEW!) ! If previous

    SW, activation does not happen until…
  209. service-worker.js (NEW!) (waiting)

  210. service-worker.js (NEW!) (waiting) All controlled clients close…

  211. self.addEventListener('install', event => { event.waitUntil( caches.open('static').then(cache => { return cache.addAll(cacheFiles);

    }) ); }); service-worker.js (install handler detail)
  212. self.addEventListener('install', event => { event.waitUntil( caches.open('static').then(cache => { return cache.addAll(cacheFiles);

    }).then(() => self.skipWaiting()) ); }); service-worker.js (install handler detail) Make SW activate immediately
  213. methods ServiceWorkerGlobalScope properties fetch caches skipWaiting

  214. Versioning a Service Worker

  215. cleaning up after old SW caches and versions

  216. None
  217. activate event

  218. ExtendableEvent

  219. self.addEventListener('activate', event => { event.waitUntil( // Clean up after old

    service worker versions ); });
  220. const cachePrefix = 'thisIsMyVersion'; Update the version string each time

    you change the service worker
  221. Use the prefix caches.open(`${cachePrefix}-static`)

  222. [ ] , , caches.keys()

  223. self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheKeys => { }) );

    }); service-worker.js (activate handler detail)
  224. self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheKeys => { var oldCacheKeys

    = cacheKeys.filter(key => { return (key.indexOf(cachePrefix) !== 0); }); }) ); }); service-worker.js (activate handler detail) Array.prototype.filter
  225. self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheKeys => { var oldCacheKeys

    = cacheKeys.filter(key => { return (key.indexOf(cachePrefix) !== 0); }); var deletePromises = oldCacheKeys.map(oldKey => { return caches.delete(oldKey); }); }) ); }); Array.prototype.map service-worker.js (activate handler detail)
  226. self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheKeys => { var oldCacheKeys

    = cacheKeys.filter(key => { return (key.indexOf(cachePrefix) !== 0); }); var deletePromises = oldCacheKeys.map(oldKey => { return caches.delete(oldKey); }); return Promise.all(deletePromises); }) ); }); service-worker.js (activate handler detail)
  227. methods ServiceWorkerGlobalScope properties fetch caches skipWaiting clients

  228. clients.claim()

  229. self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheKeys => { var oldCacheKeys

    = cacheKeys.filter(key => { return (key.indexOf(cachePrefix) !== 0); }); var deletePromises = oldCacheKeys.map(oldKey => { return caches.delete(oldKey); }); return Promise.all(deletePromises); }).then(() => self.clients.claim()) ); }); Make SW take control immediately
  230. That’s it for today

  231. But that’s not all!

  232. bit.ly/sw-smashing-nyc

  233. • Recipes for: • creating an offline image • Using

    an external JSON file for managing application shell asset URLs • Links to resources and documentation Repository also includes:
  234. Pragmatist’s Guide to Service Worker I am Lyza Danger Gardner

    I am an Open Web Engineer at Bocoup I can be found at @lyzadanger and https://www.lyza.com Smashing Conference NYC 2017 http://bit.ly/sw-smashing-nyc