Service worker not caching assets on homepage without trailing slash

649 views Asked by At

I am trying to add a service worker to part of our site that runs under an app and it's all working except for the homepage without a trailing slash - it will cache the page but not download any of the assets unless I add a slash to the url (which I can't do as the live environment removes it)

I have added the service worker allowed header to the site for the app:

<add name="Service-Worker-Allowed" value="/app-name" />

and changed the scope to allow for slashless url:

// APP_NAME is global for app-name
navigator
  .serviceWorker
  .register(`/${APP_NAME}/service-worker.js`, { scope: `/${APP_NAME}` });

And this works as I can browse to http://localhost/app-name offline, but it doesn't have any css, js or images.

If I browser to http://localhost/app-name/ I can see the cache gets updated with all the files. Is there a way to make the service worker download the files for the homepage without a slash?

Service worker code:

(() => {
  'use strict';

  const APP_NAME = 'app-name'; // define this as not compiled with webpack so no global
  const version = 1;
  const cacheName = `${APP_NAME}-cache`
  const cacheNameWithVersion = `${cacheName}-${version}`
  const offlineUrl = `/${APP_NAME}/offline`;
  const urlsToCache = [
    `/${APP_NAME}`,
    offlineUrl,
  ];
  const urlsToIgnore = [
    'google-analytics.com',
    'googletagmanager.com',
  ];

  self.addEventListener('install', event => {
    event.waitUntil(
      caches.open(cacheNameWithVersion)
        .then(cache => {
          return cache.addAll(urlsToCache);
        })
        // Force the waiting service worker to become the active service worker.
        .then(self.skipWaiting())
    );
  });

  self.addEventListener('activate', event => {
    event.waitUntil(
      caches.keys()
        .then(keys => {
          // Remove caches whose name is no longer valid
          return Promise.all(keys
            .filter(key => {
              return key.indexOf(cacheName) === 0 && key.indexOf(cacheNameWithVersion) === -1;
            })
            .map(key => {
              return caches.delete(key);
            })
          );
        })
    );
  });

  self.addEventListener('fetch', event => {
    // don't load assets from these urls
    for (let i = 0; i < urlsToIgnore.length; i++) {
      if (event.request.url.indexOf(urlsToIgnore[i]) > -1) {
        return;
      }
    }

    if (event.request.method !== 'GET') {
      // Always fetch non-GET requests from the network
      event.respondWith(
        fetch(event.request)
          .catch(() => {
            return caches.match(offlineUrl);
          })
      );

      return;
    }

    // Only fetch non-GET requests offline
    if (event.request.url.match(/\.(jpe?g|png|gif|svg)$/)) {
      // handle images or show offline svg if image isn't cached
      event.respondWith(
        caches.match(event.request)
          .then(response => {
            return response || fetch(event.request)
              .then(response => {
                addToCache(event.request, response);
                return response || serveOfflineImage();
              })
              .catch(() => {
                return serveOfflineImage();
              });
          })
      );
    } else if (event.request.mode === 'navigate' || event.request.method === 'GET') {
      // handle pages and assets like js and css
      event.respondWith(
        fetch(event.request)
          .then(response => {
            // Stash a copy of this page in the cache
            addToCache(event.request, response);
            return response;
          })
          .catch(() => {
            return caches.match(event.request)
              .then(response => {
                return response || caches.match(offlineUrl);
              });
          })
      );
    }
  });

  function addToCache(request, response) {
    if (!response.ok && response.type !== 'opaque')
      return;

    const copy = response.clone();
    caches.open(cacheNameWithVersion)
      .then(cache => {
        if (!/^https?:$/i.test(new URL(request.url).protocol)) return;
        cache.put(request, copy);
      });
  }

  function serveOfflineImage() {
    return new Response('<svg role="img" aria-labelledby="offline-title" viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg" class="object-fit"><title id="offline-title">Offline</title><g fill="none" fill-rule="evenodd"><path fill="#D8D8D8" d="M0 0h400v300H0z"/><text fill="#9B9B9B" font-family="Roboto,Arial,sans-serif" font-size="72" font-weight="bold"><tspan x="93" y="172">offline</tspan></text></g></svg>', { headers: { 'Content-Type': 'image/svg+xml' } });
  }
})();
1

There are 1 answers

1
Pete On BEST ANSWER

Looks like you always need to refresh the page that registers the service worker in order to download the assets on that page:

The Service worker will now control pages, but only those opened after the register() is successful. i.e. a document starts life with or without a Service worker and maintains that for its lifetime. So documents will have to be reloaded to actually be controlled.

Above text is point 7

I have added self.clients.claim() to the activate function which has allowed the service worker to take control of the registering page straight away, which allows some assets that are lazy loaded or loaded after the service worker to be cached without a refresh, but the rest are only cached after the refresh

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys()
    .then(keys => {
      // Remove caches whose name is no longer valid
      return Promise.all(keys
        .filter(key => {
          return key.indexOf(cacheName) === 0 && key.indexOf(cacheNameWithVersion) === -1;
        })
        .map(key => {
          return caches.delete(key);
        })
      );
    })
    // Retrieve assets on page that registered service worker
    .then(self.clients.claim())
  );
});