Search code examples
javascripthtmlservice-workerinterceptorservice-worker-events

replace asset in service worker with fetch event


I'm trying to hot switch the theme stylesheet with my service worker.

I created a theme for Jekyll (to use with GitHub Pages), and wanted the ability to replace between dark theme and light theme ("turn off the lights") and make that consistent.

Right now, this is achieved by saving the state to localstorage and replacing the theme link element according to that.

the problem is, that the check happens after the original white theme is fetched so the user sees a flash of the white theme even if localstorage have the dark theme set

here's how it behaves at the moment (the buttons are on the top right): https://thatkookooguy.github.io/postmortem-demo/postmortem/002

after changing the theme, try navigating to another page. the page loads with the white theme, and after that changes to the dark theme

I thought I can solve this problem by switching the script inside a service worker, since I can addEventListener to the fetch event and replace it in the promise chain with another fetch.

But the code doesn't seem to work. If I add a debug point inside the event callback, it's never triggered (not on assets and not on actual page fetches)

HTML Head

<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <link rel='stylesheet prefetch' href='//fonts.googleapis.com/css?family=Fredoka+One|Righteous'>
  <!-- THIS SHOULD BE INTERCEPTED BY THE SERVICE WORKER -->
  <link id="bulma-theme" rel="stylesheet prefetch" href="//unpkg.com/bulmaswatch/default/bulmaswatch.min.css">
  <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
  <link rel="stylesheet" type="text/css" href="{{ site.baseurl }}/assets/style.css" />
  <script>
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker
        .register('{{ site.baseurl }}/assets/sw.js')
        .then(function() {
          console.log("Service Worker Registered");
        });
    }
  </script>
</head>

Service Worker Code

importScripts('localforage.min.js');

// remember the theme
this.onmessage = function(msg) {
  if (!msg.data.theme) {
    return;
  }

  localforage.setItem('theme', msg.data.theme);
}

// replace link fetching with correct theme
this.addEventListener('fetch', function(event) {
  console.log(event.request.url);

  if (event.request.url.contains('/bulmaswatch/')) {
    return localforage.getItem('theme')
      .then(function(theme) {
        if (theme === 'kb-dark-theme') {

          let darkThemeRequest = new Request(request.url.replace('bulmaswatch/default/', 'bulmaswatch/superhero/'), {
            method: request.method,
            headers: request.headers,
            mode: 'same-origin',
            credentials: request.credentials,
            redirect: 'manual'
          });

          return fetch(darkThemeRequest);
        }

        return fetch(event.request);
      })
  }
});

// Log SW installation
this.addEventListener('install', function(event) {
  console.log('[SW]: installing....');
});

// Log SW activation
this.addEventListener('activate', function activator(event) {
  console.log('[SW]: activate!');
});

When debugging in chrome (and chrome canary, since I heard there's better service worker debugging support there), the callback on the fetch even never called. no request was ever logged.

I do see the service worker registered and active in chrome's dev tools. and I see the other events logging in the console.

Also, when debugging and putting a breakpoint at the start of the script, it looks like the bulmaswatch.min.css was already fetched.

any idea what I'm missing? :-)

thanks


Solution

  • The underlying issue has to do with the scope of your service worker.

    When I visit https://thatkookooguy.github.io/postmortem-demo/postmortem/002 and look in the Application > Service Workers panel in Chrome's DevTools, I see that the service worker has a scope of https://thatkookooguy.github.io/postmortem-demo/assets and does not control any clients.

    Screenshot of Application panel

    If you move your sw.js file out of the /assets/ subdirectory, and into the top-level directory for your site (which is /postmortem-demo/), and then switch your registration code to reflect that, then your service worker should be able to control your client pages:

    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/postmortem-demo/sw.js');
    }
    

    (This means that when developing locally, you should also serve your web app with a URL prefix of /postmortem-demo/ to match that.)

    The following resources have more info about service worker scoping: