Denno
Denno

Reputation: 2178

Workbox service worker breaking SPA routes

I've set up Workbox using InjectManifest (just setting properties swSrc and swDest), and created my service worker (attached below).

Everything works great when I start from the root of the site, but if I start from another page, one that is otherwise handled by React Router (for example, by reloading the page), the service worker gives me the error: Cannot get /page, where "page" is the URL route that I'm trying to load.

As far as I can tell, I don't have anything in my service worker that would indicate how these routes should be handled, but it seems that the service worker is preventing the index.html file from being loaded when a random URL string is being requested.

I have tried to use registerRoute(new NavigationRoute(createHandlerBoundToURL('/index.html'))), but then I get an error that index.html isn't pre-cached. I don't want to cache index.html, as I don't want to end up in the situation where a new service worker will never be downloaded because an old cached page is always being served up..

What else can be done?

import { CacheableResponsePlugin } from 'workbox-cacheable-response/CacheableResponsePlugin';
import { CacheFirst } from 'workbox-strategies/CacheFirst';
import { StaleWhileRevalidate } from 'workbox-strategies/StaleWhileRevalidate';
import { ExpirationPlugin } from 'workbox-expiration/ExpirationPlugin';
import { precacheAndRoute } from 'workbox-precaching/precacheAndRoute';
import { registerRoute } from 'workbox-routing/registerRoute';
import { setCatchHandler } from 'workbox-routing/setCatchHandler';
import { setDefaultHandler } from 'workbox-routing/setDefaultHandler';
import { clientsClaim } from 'workbox-core';
import { NetworkOnly } from 'workbox-strategies';

const imageFallback = `/media/catalog/product/p/l/placeholder.jpg`;

precacheAndRoute(self.__WB_MANIFEST);
clientsClaim();
setDefaultHandler(new NetworkOnly());

registerRoute(
    new RegExp('(robots.txt|favicon.ico|manifest.json)'),
    new StaleWhileRevalidate()
);

// Handle images hosted with the PWA, that means they'll be served from the same origin
registerRoute(
    ({ url, sameOrigin }) => {
        // Only cache images from the same origin
        if (!sameOrigin) return false;

        return url.pathname.match(/\.(?:png|gif|jpg|jpeg|svg|pjpg|webp)$/);
    },

    new StaleWhileRevalidate({
        cacheName: 'local-images',
        matchOptions: {
            ignoreVary: true
        },
        plugins: [
            new ExpirationPlugin({
                // Keep at most 100 entries
                maxEntries: 100,
                // Don't keep any entries for more than 7 days
                maxAgeSeconds: 7 * 24 * 60 * 60,
                // Automatically cleanup if quota is exceeded
                purgeOnQuotaError: true
            }),
            new CacheableResponsePlugin({
                statuses: [0, 200]
            })
        ]
    })
);

// Handle images loaded from the BE URL. This likely means product images
registerRoute(
    ({ url }) => {
        if (url.origin !== __MAGENTO_BE_URL__) return false;

        return url.pathname.match(/\.(?:png|gif|jpg|jpeg|svg|pjpg|webp)$/);
    },

    new StaleWhileRevalidate({
        cacheName: 'remote-images',
        matchOptions: {
            ignoreVary: true
        },
        plugins: [
            new ExpirationPlugin({
                // Keep at most 100 entries
                maxEntries: 100,
                // Don't keep any entries for more than 7 days
                maxAgeSeconds: 7 * 24 * 60 * 60,
                // Automatically cleanup if quota is exceeded
                purgeOnQuotaError: true
            }),
            new CacheableResponsePlugin({
                statuses: [0, 200]
            })
        ]
    })
);

registerRoute(new RegExp(/\.js$/), new CacheFirst());

self.addEventListener('fetch', event => {
    const { request } = event;
    const responsePromise = router.handleRequest({
        event,
        request
    });
    if (responsePromise) {
        // Router found a route to handle the request.
        event.respondWith(responsePromise);
    } else {
        // No route was found to handle the request.
        // Fallback to network
    }
});

const catchHandler = async options => {
    const dest = options.request.destination;
    const cache = await self.caches.open('workbox-offline-fallbacks');

    if (dest === 'image' && imageFallback !== false) {
        return (await cache.match(imageFallback)) || Response.error();
    }

    return Response.error();
};

setCatchHandler(catchHandler);

self.addEventListener('install', event => {
    const files = [];
    if (imageFallback) {
        files.push(imageFallback);
    }

    event.waitUntil(
        self.caches
            .open('workbox-offline-fallbacks')
            .then(cache => cache.addAll(files))
    );
});

self.addEventListener('message', event => {
    if (event.data && event.data.type === 'SKIP_WAITING') {
        self.skipWaiting();
    }
});

Upvotes: 1

Views: 1880

Answers (1)

Stef Chäser
Stef Chäser

Reputation: 2058

Your concerns about nothing is updating anymore are truly valid, there are a lot of pitfalls when working with service worker.

I think your index.html is probably automatically added to the cache with

precacheAndRoute(self.__WB_MANIFEST);

Anyhow, it should be safe to add index.html to the cache, because it is not index.html which decides if there is a new service-worker version, it is the sw.js file itself. So when /page is called your service worker can respond with index.html

Most common pitfalls:

You should not rename or relocate the sw.js file in a newer version of your site. Because older, cached versions in the users browser will still look for the old location.

Another thing to keep an eye on is to not mix up cached files with different versions of your site. When publishing and installing a new service worker make sure you clean/update the cache appropriate. Keep in mind that also the HttpCache of the browser could possibly cache "old" versions of your files. A solution to this problem is to add a file hash to your filename stlyes.css => styles-e34f44de.css

Last, but not least, a good idea is to implement a reset button, which unregisters the service worker and cleans the cache. (In case of a mess up, it is much easier to explain your user to click this button, than explain how to opening devtools and reset everything, especially on mobile)

Upvotes: 2

Related Questions