Service Workers

A service worker is a script that your browser runs in the background, separate from a web page, opening the door to features that don’t need a web page or user interaction.

Google provides a set of tools for working with service workers called workbox.

The examples below use the workbox library.

Service worker setup for django

Register the route in the project URLs:


# project_root/urls.py

from django.views.generic import TemplateView

urlpatterns += i18n_patterns(
    url(r'^service-worker.js', (TemplateView.as_view(
        template_name="service-worker.js",
        content_type='application/javascript',
    )), name='service-worker.js'),
)

Once we’ve done that then we can add a the template which contains the service worker.

        <!-- bottom of base.html -->

        <script>
        // Check that service workers are supported
        if ('serviceWorker' in navigator) {
          // Use the window load event to keep the page load performant
          window.addEventListener('load', () => {
            navigator.serviceWorker.register('/en/service-worker.js');
          });
        }
        </script>

Service worker script

Example file


// Example service worker
// https://developers.google.com/web/tools/workbox/guides/get-started
importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.0.0/workbox-sw.js');

const setCacheNameDetails = workbox.core.setCacheNameDetails;
const precacheAndRoute = workbox.precaching.precacheAndRoute;
const registerRoute = workbox.routing.registerRoute;
const StaleWhileRevalidate = workbox.strategies.StaleWhileRevalidate;
const NetworkFirst = workbox.strategies.NetworkFirst;
const CacheFirst = workbox.strategies.CacheFirst;
const NetworkOnly = workbox.strategies.NetworkOnly;
const CacheableResponsePlugin = workbox.cacheableResponse.CacheableResponsePlugin;
const ExpirationPlugin = workbox.expiration.ExpirationPlugin;
const setDefaultHandler = workbox.routing.setDefaultHandler;
const setCatchHandler = workbox.routing.setCatchHandler;


setCacheNameDetails({
  prefix: 'myapp',
  suffix: 'v3',
});

// You need to create this page in django
const FALLBACK_HTML_URL = '/en/offline';

// Precache assets
workbox.precaching.precacheAndRoute([
  { 'url': FALLBACK_HTML_URL, 'revision': 1 },
]);

const adminNoCache = () => {

  // only ever use network for django admin
  registerRoute(
    ({ url }) =>
      url.pathname.includes('/admin/') ||
      url.pathname.includes('/cms_login/') ||
      url.pathname.includes('/cms_wizard/') ||
      url.searchParams.has('structure'),
    new NetworkOnly(),
  );
}

adminNoCache();

// https://developers.google.com/web/tools/workbox/modules/workbox-recipes#google_fonts_cache
const googleFontCache = () => {

  let sheetCacheName = 'google-fonts-stylesheets';
  let fontCacheName = 'google-fonts-webfonts';
  let maxAgeSeconds = 60 * 60 * 24 * 365;
  let maxEntries = 30;

  registerRoute(
    ({ url }) => url.origin === 'https://fonts.googleapis.com',
    new workbox.strategies.StaleWhileRevalidate({
      cacheName: sheetCacheName,
    }),
  );

  // Cache the underlying font files with a cache-first strategy for 1 year.
  registerRoute(
    ({ url }) => url.origin === 'https://fonts.gstatic.com',
    new CacheFirst({
      cacheName: fontCacheName,
      plugins: [
        new CacheableResponsePlugin({
          statuses: [0, 200],
        }),
        new ExpirationPlugin({
          maxAgeSeconds,
          maxEntries,
        }),
      ],
    }),
  );
}

googleFontCache();

// https://developers.google.com/web/tools/workbox/modules/workbox-recipes#static_resources_cache
const staticResourceCache = () => {

  let matchCallback = ({ request }) =>
    // CSS
    request.destination === 'style' ||
    // JavaScript
    request.destination === 'script' ||
    // Web Workers
    request.destination === 'worker';

  registerRoute(
    matchCallback,
    new StaleWhileRevalidate({
      cacheName: 'static-resources',
      plugins: [
        new CacheableResponsePlugin({
          statuses: [0, 200],
        }),
      ],
    }),
  );

}

staticResourceCache();

// https://developers.google.com/web/tools/workbox/modules/workbox-recipes#image_cache
const imageCache = () => {

  registerRoute(
    ({ request }) => request.destination === 'image',
    new CacheFirst({
      cacheName: 'images',
      plugins: [
        new CacheableResponsePlugin({
          statuses: [0, 200],
        }),
        new ExpirationPlugin({
          maxEntries: 60,
          maxAgeSeconds: 30 * 24 * 60 * 60,
        }),
      ],
    }),
  );
}

imageCache();

// main document caching
// Network first
// check for status 200 and check for X-Is-Cacheable header
// updated based on this recipe:
// https://developers.google.com/web/tools/workbox/modules/workbox-recipes#pattern_3

const documentCache = () => {

  const networkTimeoutSeconds = 3;

  registerRoute(
    ({ request }) => request.mode === 'navigate' 
      || request.destination === 'document',
    new NetworkFirst({
      networkTimeoutSeconds,
      cacheName: 'document-cache',
      plugins: [
        new CacheableResponsePlugin({
          statuses: [0, 200],
          headers: {
            'X-Is-Cacheable': 'true'
          }
        })
      ]
    })
  );

}

documentCache();

// Use a network first strategy for all other requests as we don't know whether
// they have any other side affects (such as containing a CSRF token etc)
setDefaultHandler(
  new NetworkFirst({
    plugins: [
      new CacheableResponsePlugin({
        statuses: [0, 200],
        headers: {
          'X-Is-Cacheable': 'true'
        }
      })
    ]
  }),
);

// https://developers.google.com/web/tools/workbox/guides/advanced-recipes
setCatchHandler(({ event }) => {
  switch (event.request.destination) {
    case 'document':
      return workbox.precaching.matchPrecache(FALLBACK_HTML_URL);
      break;

    // case 'image':
    //   // If using precached URLs:
    //   // return matchPrecache(FALLBACK_IMAGE_URL);
    //   return caches.match(FALLBACK_IMAGE_URL);
    // break;

    default:
      return Response.error();
  }
});


Unregistering service workers

I found that I needed to uninstall a service worker that we causing problems and found this stack overflow post really useful: https://stackoverflow.com/questions/33704791/how-do-i-uninstall-a-service-worker

In the end this is the solution I went with:


<script>
    if ('serviceWorker' in navigator) {
      // Use the window load event to keep the page load performant
      navigator.serviceWorker.getRegistrations().then(function(registrations) {
        for(let registration of registrations) {
            registration.unregister();
        } 
      });
    }
</script>