Service Workers

The Workering

Ben Kelly (@wanderview)

What are service workers?

Standardized Web API

Run JS in the background

Work offline

Receive push notifications

And much more...

Why do we need this thing?

2014: The web is doomed!

2016: Not dead yet..

2016: Apps are starting to saturate

What if we could have the best of both worlds?

Progressive Web Apps

PWA

PoWA?

P - W - A

How does all this work?

Workers execute JS on separate threads

See Nolan Lawson's (@nolanlawson) blog and demo about moving storage work to a separate thread.

Things to note about dedicated Workers:

  • Created directly via - new Worker(scriptURL)
  • Threads live until Worker object is garbage collected
  • Closing the owning window will terminate the thread

Service Workers are Different

  • can run when you have no window for the site
  • threads are killed and restarted often
  • browser manages code updates

Service Worker Lifecycle

What happens during an update?

Registering a Service Worker


        // In your index.html
        if (navigator.serviceWorker) {
          navigator.serviceWorker.register('sw.js').then(swr => {
            console.log('registered service worker!');
          }).catch(e => {
            console.log('failed to register service worker!');
          });
        }
        
  • Requires https
  • Can use http://localhost for development, but not file://
  • Can scope the registration as well, but lets ignore that for now

Lifecycle Events


        // In your service worker script

        self.addEventListener('install', evt => {
          console.log('Installing!');
        });

        self.addEventListener('activate', evt => {
          console.log('Activating!');
        });
        

Worker thread can be killed while doing async operations

waitUntil() to the rescue!


        // In your service worker script

        function asyncWork() {
          return new Promise(resolve => { setTimeout(resolve, 5000); });
        }

        self.addEventListener('install', evt => {
          console.log('Install starting');
          evt.waitUntil(asyncWork().then(_ => {
            console.log('Install complete!');
          });
        });

        self.addEventListener('activate', evt => {
          console.log('Activate starting!');
          evt.waitUntil(asyncWork().then(_ => {
            console.log('Activate complete!');
          });
        });
        

Fetch API


        fetch('http://example.com/my/rest/api').then(response => {
          return response.text();
        }).then(text => {
          console.log(text);
        }).catch(e => {
          console.error(e);
        });
        

See MDN for more documentation.

Cache API


        var url = 'http://example.com/my/asset';
        var cache;
        caches.open('my-cache').then(c => {
          cache = c;
          return fetch(url);
        }).then(response => {
          return cache.put(url, response);
        }).then(_ => {
          cache.match(url);
        }).then(response => {
          return response.text();
        }).then(text => {
          return cache.delete(url);
        }).then(result => {
          // result should be true
          return caches.delete('my-cache');
        }).then(result => {
          // result should be true
        });
        
  • Storage API like IndexedDB
  • See MDN for more documentation.

Pre-cache Assets in the Install Event


        var cacheName = 'myCache';

        var assets = [
          'http://example.com/my/asset/1',
          'http://example.com/my/asset/2',
        ];

        self.addEventListener('install', evt => {
          evt.waitUntil(caches.open(cacheName).then(cache => {
            var requests = assets.map(url => new Request(url, { cache: 'no-cache' }));
            cache.addAll(requests);
          });
        });
        

Cleanup old Assets in the Activate Event


        var cacheName = 'myCache';

        self.addEventListener('activate', evt => {
          evt.waitUntil(caches.keys().then(list => {
            return Promise.all(
              list.map(name => {
                if (name === cacheName) {
                  return;
                }
                return caches.delete(name);
              });
            );
          });
        });
        

Fetch Event


        self.addEventListener('fetch', evt => {
          console.log('Network request for ' + evt.request.url);
        });
        
  • Fired when the browser makes a network request
  • FetchEvent.request holds the original request
  • FetchEvent.respondWith() allows you to provide a custom response
  • FetchEvent.respondWith() also acts like waitUntil()

Which Network Request Get a Fetch Event?

  • Top level requests that match the service worker registration scope
    • Document index.html requests
    • Worker script requests
  • Once the top level request matches, the document or worker is said to be "controlled" by the service worker.
  • navigator.serviceWorker.controller
  • All network requests made by that document or worker are then also intercepted by the service worker.

Offline Pre-Cached Assets


        self.addEventListener('fetch', evt => {
          evt.respondWith(caches.open(myCache).then(cache => {
            // First try to find a pre-cached response
            return cache.match(evt.request);
          }).then(response => {
            // Fallback to network
            return response || fetch(evt.request);
          }).catch(e => {
            // Fallback to network if we hit an unexpected error
            return fetch(evt.request);
          }));
        });
        

This slide deck uses a service worker.

Check it out!

What about push notifications?

Moar Resources!