Service workers

Valentin Goșu

valentin.gosu@gmail.com

 

What is it good for?

Offline

Performance

Push & Notifications

Application Cache

DEPRECATED!

CACHE MANIFEST
# 2010-06-18:v3

# Explicitly cached entries
index.html
css/style.css

# offline.html will be displayed if the user is offline
FALLBACK:
/ /offline.html

# All other resources (e.g. sites) require the user to be online. 
NETWORK:
*

# Additional resources to cache
CACHE:
images/logo1.png
images/logo2.png
images/logo3.png

"Application Cache is a Douchebag"

-Jake Archibald

Application Cache

Deprecated: Firefox | HTML Spec

Secure only!

Exception: localhost

Try letsencrypt.org

Github Pages available over HTTPS

'use strict';

if (window.location.hostname != 'localhost' && window.location.protocol == 'http:') {
    // If we're on github, make sure we're on https
    window.location.protocol = "https";
}
'use strict';

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('serviceWorker.js').then(function(registration) {
    console.log('ServiceWorker registration successful with scope: ', registration.scope);
  }).catch(function(err) {
    console.log('ServiceWorker registration failed: ', err);
  });
}

/hello-world/hello.js

Register the service worker

var promise = new Promise(function(resolve, reject) {
  resolve(1);
});

promise.then(function(val) {
  console.log(val); // 1
  return val + 2;
}).then(function(val) {
  console.log(val); // 3
});

Promises

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.log("Failed!", error);
});
get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
});
get('story.json').then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
});
'use strict';

self.addEventListener('install', event => {
  console.log('service worker - install');
  function onInstall () {
    return caches.open('static')
      .then(cache =>
        cache.addAll([
          'hello.js', // Using relative path. Could also say /hello-world/hello.js
          'index.html'
        ])
      );
  }

  event.waitUntil(onInstall());
});

self.addEventListener('activate', event => {
  console.log('service worker - activate');
});

/hello-world/serviceWorker.js

 

Handle install event

self.addEventListener('install', event => {
  // Skip waiting. Activate immediately.
  event.waitUntil(
    onInstall() // add statics to cache
     // and trigger activate immediately
     .then( () => self.skipWaiting() )
  );
});

self.addEventListener('activate', event => {
  function onActivate () {
    // Maybe cleanup old caches
  }
  event.waitUntil(
    onActivate()
     // This makes the SW take effect immediately
     // on any open pages in scope
     .then( () => self.clients.claim() )
  );
});

/hello-world/serviceWorker.js

 

Handle install event - faster

Debug

Firefox

about:serviceworkers

Chrome

chrome://serviceworker-internals

Debug

Firefox

Devtools > Storage > Cache Storage

Chrome

Devtools > Resources > Cache Storage

Debug

self.addEventListener('fetch', event => {

  function shouldHandleFetch (event) {
    // Should we handle this fetch?
  }

  function onFetch (event) {
    // TODO: Respond to the fetch
  }

  if (shouldHandleFetch(event)) {
    onFetch(event);
  }
});

/hello-world/serviceWorker.js

 

Handle fetch event

function shouldHandleFetch (event) {
  // Should we handle this fetch?
  var request            = event.request;
  var url                = new URL(request.url);

  // Your criteria:
  //   * what do you want to intercept?
  //   * can't cache resources from other domains

  // We don't want to intercept the fetch of the service worker,
  // or we might not get any updates :)
  return !(url.href == serviceWorkerAddress || 
           (url.hostname != 'localhost' && url.hostname != 'awesome-sw.github.io'));
}

/hello-world/serviceWorker.js

 

Handle fetch event

function onFetch (event) {
  var request      = event.request;
  var acceptHeader = request.headers.get('Accept');
  var resourceType = 'static';
  var url          = new URL(request.url);

  if (acceptHeader.indexOf('text/html') !== -1) {
    resourceType = 'content';
  } else if (acceptHeader.indexOf('image') !== -1) {
    resourceType = 'image';
  } else if (url.pathname.indexOf('/generated/') === 0) {
    resourceType = 'generated';
  }

  // respond to fetch according to resourceType
}

/hello-world/serviceWorker.js

 

Handle fetch event

function onFetch (event) {
  // [...]
  // respond to fetch according to resourceType

  // Strategy:
  // * images - cache first
  // * content - network first
  // * generated - build a response on the fly
  // * offline fallback - not in cache, network fails
  // * modify response
  // * other
}

/hello-world/serviceWorker.js

 

Handle fetch event

function onFetch (event) {
  // [...]
  // respond to fetch according to resourceType

  // Strategy:
  // * images - cache first
  // * content - network first
  // * generated - build a response on the fly
  // * offline fallback - not in cache, network fails
  // * modify response
  // * other
}

/hello-world/serviceWorker.js

 

Handle fetch event

event.respondWith(
  // Search for the resource in the cache
  fetchFromCache(event)
    // Not in cache. Hit the network
    .catch(() => fetch(request))
    // Add response to the cache (and return the response)
    .then(response => addToCache(cacheKey, request, response))
    // Network also failed. Return a generic offline response
    .catch(() => offlineResponse(resourceType))
  );

/hello-world/serviceWorker.js

 

Cache first

event.respondWith(
  // Fetch the request from the network
  fetch(request)
    // Add the response to the cache and return it
    .then(response => addToCache(cacheKey, request, response))
    // Network failed. Search in the cache.
    .catch(() => fetchFromCache(event))
    // Cache also failed. Return a generic offline response
    .catch(() => offlineResponse(opts))
);

/hello-world/serviceWorker.js

 

Network first

function addToCache (cacheKey, request, response) {
  if (response.ok) {
    // We may get a response, that is not OK.
    // Such as a 404    

    // Create a copy of the response to put in the cache
    var copy = response.clone();
    caches.open(cacheKey).then( cache => {
      cache.put(request, copy);
    });

    // Return the response
    return response;
  }

  // The response is a 404 or other error
}

/hello-world/serviceWorker.js

 

Cache the responses we get

function fetchFromCache (event) {
  // Search for a response in all caches
  return caches.match(event.request).then(response => {
    if (!response) {
      // A synchronous error that will kick off the catch handler
      throw Error('${event.request.url} not found in cache');
    }
    // Return the response
    return response;
  });
}

/hello-world/serviceWorker.js

 

Retrieve the resource from the cache

function offlineResponse (resourceType) {
  if (resourceType === 'image') {
    // synchronously build a response
    return new Response(svgImageDefinition,
      { headers: { 'Content-Type': 'image/svg+xml' } }
    );
  } else if (resourceType === 'content') {
    // return another 
    return caches.match(staticOfflinePage);
  }
  return undefined;
}

/hello-world/serviceWorker.js

 

Generate an offline response

var endpoint;
navigator.serviceWorker.register('serviceWorker.js').then(function(registration) {
  return registration.pushManager.getSubscription()
  .then(function(subscription) {
    // If a subscription was found, return it.
    if (subscription) {
      return subscription;
    }

    // Otherwise, subscribe the user
	return registration.pushManager.subscribe({ userVisibleOnly: false });
  });
}).then(function(subscription) {
  // This is the URL of the endpoint we need to call to get a notification
  endpoint = subscription.endpoint;
});

/hello-world/hello.js

Push & Notifications

self.addEventListener('push', function(event) {
  var payload = event.data ? event.data.text() : 'Alea iacta est';
  event.waitUntil(
     // There are many other possible options, for an exhaustive list see the specs:
     //   https://notifications.spec.whatwg.org/
     self.registration.showNotification('ServiceWorker Cookbook', {
      lang: 'la',
      body: payload,
      icon: 'caesar.jpg',
      vibrate: [500, 100, 500],
     })
  );
})

/hello-world/serviceWorker.js

Push & Notifications

self.addEventListener('push', function(event) {
  var payload = event.data ? event.data.text() : 'Alea iacta est';
  
  // may haves several pages and just one serviceWorker
  clients.matchAll().then(function(clients){
    clients[0].postMesssage(payload);
  });
})

/hello-world/serviceWorker.js

Trigger events

navigator.serviceWorker.addEventListener('message', function(e) {
    console.log(e.data);
});

/hello-world/hello.js

curl --request POST -H "TTL: 60"
https://updates.push.services.mozilla.com/push/XXX...YYY=

curl

Send a notification (Firefox)

fetch(endpoint, {
	method: "POST",
	headers: {
		"TTL": "60"
	}
});

fetch it!

curl --header "Authorization: key=AIzaSyBBh4ddPa96rQQNxqiq_qQj7sq1JdsNQUQ"
     --header "Content-Type: application/json"
     https://android.googleapis.com/gcm/send
     -d "{\"registration_ids\":[\"XXXYYY\"]}"

curl

Send a notification (Chrome)

Run your own server

curl https://serviceworke.rs/push-payload/sendNotification \
     -H 'Content-Type: application/json' \
     -d '{"endpoint":"https://updates.push.services.mozilla.com/push/...1Kw=",
"key":"B...Yqar9yAQaN9E=","payload":"Insert here a payload","delay":"5","ttl":"0"}'

curl

Send a notification with payload (Firefox)

fetch("https://serviceworke.rs/push-payload/sendNotification", {
	method: "POST",
	headers: {
                "Content-Type": "application/json"
	},
	body: '{"endpoint":"https://updates.push.services.mozilla.com/push/g...w=",
                "key":"B...E=",
                "payload":"Insert here a payload","delay":"5","ttl":"0"}'
}).then(e => console.log(e));

fetch it!

.then(function(subscription) {
    // This is the URL of the endpoint we need to call to get a notification
    console.log('endpoint: ', subscription.endpoint);

    var rawKey = subscription.getKey ? subscription.getKey('p256dh') : '';
    var key = rawKey ?
        btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) :
        '';
    console.log('key: ', key);
  });

get the key

Send a notification with payload (Firefox)

self.addEventListener('push', function(event) {
  event.waitUntil(
    getEndpoint()
    .then(function(endpoint) {
      return fetch('./getPayload?endpoint=' + endpoint);
    })
    .then(function(response) {
      return response.text();
    })
    .then(function(payload) {
      self.registration.showNotification('ServiceWorker Cookbook', {
        body: payload,
      });
    })
  );
});

serviceWorker.js

Send a notification with payload (Chrome)

  app.post(route + 'sendNotification', function(req, res) {
    setTimeout(function() {
      payloads[req.body.endpoint] = req.body.payload;
      webPush.sendNotification(req.body.endpoint, req.body.ttl)
      .then(function() {
        res.sendStatus(201);
      });
    }, req.body.delay * 1000);
  });

  app.get(route + 'getPayload', function(req, res) {
    res.send(payloads[req.query.endpoint]);
  });

./getPayload

Send a notification with payload (Chrome)

Demo

References

Questions