Building an Offline Page for theguardian.com — Front-end London, November 2015

Building an Offline Page for theguardian.com — Front-end London, November 2015

You’re on a train to work and you open up the Guardian app on your phone. A tunnel surrounds you, but the app still works in very much the same way as it usually would—despite your lack of internet connection, you still get the full experience, only the content shown will be stale. If you tried the same for the Guardian website, however, it wouldn’t load at all. Native apps have long had the tools to deal with these situations, in order to deliver rich user experiences whatever the user’s situation may be. With service workers, the web is catching up.

D97317c84e6f6bbccb2732b619648c03?s=128

Oliver Joseph Ash

November 27, 2015
Tweet

Transcript

  1. Building an Offline Page for theguardian.com Oliver Joseph Ash –

    Front-end London, November 2015
 @OliverJAsh
  2. offline

  3. server down

  4. poor connection

  5. • Content is cached • Experience: • offline: stale content

    remains • server down: stale content remains • poor connection: stale while revalidate • good connection: stale while revalidate Native app
  6. Experience: • offline: nothing • server down: nothing • poor

    connection: white screen of death • good connection: new content Website
  7. We can do better!

  8. None
  9. How it works

  10. What is a service worker? • Script that runs in

    the background • Useful for features with no user interaction, e.g.: • listen to push events, useful for pushing notifications • intercept and handle network requests • future: • background sync • geofencing • Trusted origins only (HTTPS, localhost) • Chrome stable, Firefox nightly
  11. <script> if (navigator.serviceWorker) { navigator.serviceWorker.register('/service-worker.js'); } </script> 1. Create and

    register the service worker
  12. 1 2

  13. None
  14. var version = 1; var staticCacheName = 'static' + version;

    var updateCache = function () { return caches.open(staticCacheName) .then(function (cache) { return cache.addAll([ '/offline-page', '/assets/css/main.css', '/assets/js/main.js' ]); }); }; self.addEventListener('install', function (event) { event.waitUntil(updateCache()); }); 2. Get ready with install • install event: get ready! • Cache the assets needed later • Version the cache
  15. 1 2

  16. 3. Intercept requests with fetch

  17. const staticCacheName = 'static'; const version = 1; const updateCache

    = () => ( caches.open(staticCacheName + version) .then(cache => cache.addAll([ '/offline-page.html', '/assets/css/main.css', '/assets/js/main.js' ]); ); ); self.addEventListener('install', function (event) { event.waitUntil(updateCache()); }); self.addEventListener('fetch', function (event) { event.respondWith(fetch(event.request)); }); • Default: just fetch • Override default • Intercept network requests to: • fetch from the network • read from the cache • construct your own response fetch events
  18. None
  19. • Respond with a own response • e.g., use templating

    to construct a HTML response from JSON Service worker: custom responses self.addEventListener('fetch', function (event) { var responseBody = '<h1>Hello, World!</h1>'; var responseOptions = { headers: { 'Content-Type': 'text/html' } }; var response = new Response( responseBody, responseOptions ); event.respondWith(response); });
  20. HTML

  21. var doesRequestAcceptHtml = function (request) { return request.headers.get('Accept') .split(',') .some(function

    (type) { return type === ‘text/html’; }); }; self.addEventListener('fetch', function (event) { var request = event.request; if (doesRequestAcceptHtml(request)) { event.respondWith(fetch(request)); } });
  22. None
  23. self.addEventListener('fetch', function (event) { var request = event.request; if (doesRequestAcceptHtml(request))

    { event.respondWith( fetch(request).catch(function () { return caches.match('/offline-page'); }) ); } });
  24. Assets

  25. None
  26. self.addEventListener('fetch', function (event) { var request = event.request; if (doesRequestAcceptHtml(request))

    { event.respondWith( fetch(request).catch(function () { return caches.match('/offline-page'); }) ); } else { event.respondWith( caches.match(request) ); } });
  27. None
  28. self.addEventListener('fetch', function (event) { var request = event.request; if (doesRequestAcceptHtml(request))

    { event.respondWith( fetch(request).catch(function () { return caches.match('/offline-page'); }) ); } else { event.respondWith( caches.match(request).then(function (response) { response || fetch(request); }) ); } });
  29. 4. Updating the crossword

  30. var version = 1; var cacheNameSuffix = 'static' + '-'

    + version; var getISODate = function () { return new Date().toISOString().split('T')[0]; }; var getCacheName = function () { return getISODate() + '-' + cacheNameSuffix; }; var isLatestCacheName = function (key) { return key === getCacheName(); }; var deleteOldCaches = function () { var getOldCacheKeys = function () { return caches.keys().then(function (keys) { return keys.filter(function (key) { return !isLatestCacheName(key); }); }); }; return getOldCacheKeys().then(function (oldCacheKeys) { return Promise.all(oldCacheKeys.map(function (key) { return caches.delete(key); })); }); }; var isCacheUpdated = function () { return caches.keys().then(function (keys) { return keys.some(isLatestCacheName); }); }; isCacheUpdated().then(function (isUpdated) { if (!isUpdated) { updateCache().then(deleteOldCaches); } }); // <normal fetch handling code>
  31. None
  32. Future usages of service worker on theguardian.com • Offline page

    will increase in significance • Offline page can be extended • show stale content, like native apps • show personalised content, downloaded ahead of time • show content that has been “saved for later” • Go fully offline-first
  33. Offline-first • Instantly respond to navigations with a “shell” •

    Show stale content while revalidating • Improves the experience for users with poor connections
  34. None
  35. Why? Is this valuable? • Fun • Insignificant usage due

    to HTTPS/browser support • … but good to plant the seed • Iron out browser bugs
  36. “If we only use features that work in IE8, we're

    condemning ourselves to live in an IE8 world.” — Nolan Lawson
  37. Conclusion • Service workers allow us to: • define rich

    experiences for users who are offline • improve performance for users with poor connections • It’s easy to build an offline page • Offline page is a good place to start
  38. Thank you ps., we’re hiring! developers.theguardian.com