Search code examples
javascriptwebpacksingle-page-applicationworkboxworkbox-window

Serviceworker never gets activated or receives messages but workbox functionality works on subpaths


In my current project using webpack with (workbox-webpack-plugin) and workbox (with workbox-window) I have encountered an error where the serviceworker will never respond to messages when the page is loaded at a "deep url", like "/my-path/is-nested". It will work on "/my-path".

In a minimal reproducible environment, this is supposed to happen.

  • In a new tab, app is accessed (e.g. via https://my-app.my-name.company-dev-envs.cloud/some-unique-id)
  • ServiceWorker gets registered via workbox (.register())
  • upon registration, messageSW is used to ask for the SWs "version"
  • ServiceWorker responds using event.ports[0].postMessage with a string indicating the version

Is the site accessed in a "deep" path, like /some-unique-id/test, the messaging will never work.

Workbox does successfully "register" - but no events will be fired (except externalactivated when Update on Reload in the Application > Service Worker Tab is selected) The serviceworker will be reported as "active and running" with no other service workers displayed. It seems to handle fetches okay too. Only messages and events never respond/fire via the wb Workbox Window instance.

The "application" tab reports the service worker as active and running: application > service worker tab displaying sw correctly

index.js (main entry for application)

/* globals window, System */
import './offline';

// [...]

offline.js

import { Workbox } from 'workbox-window';

if ('serviceWorker' in navigator) {
  const wb = new Workbox('./sw.js');

  const attachDebugEventHandlers = (events) => {
    events.forEach((eventName) => {
      wb.addEventListener(eventName, () => {
        console.log(`[workbox sw] ${eventName} triggered`);
      });
    });
  };

  attachDebugEventHandlers([
    'activated',
    'controlling',
    'externalactivated',
    'externalinstalling',
    'externalwaiting',
    'installed',
    'message',
    'redundant',
    'waiting',
  ]);

  wb.register()
    .then((registration) => {
      console.log('workbox sw register successful');

      console.log("sending message")
      wb.messageSW({ type: 'GET_VERSION' }).then((ver) => console.log(`[workbox sw] version reported as ${ver}`))
    })
    .catch((err) => {
      console.error('[workbox sw] could not activate sw', err);
    });

sw.js

const SW_VERSION = '1.0.0';

console.log("sw loaded")

import {createHandlerBoundToURL, precacheAndRoute} from 'workbox-precaching';
import { NavigationRoute, registerRoute } from 'workbox-routing'

// auto generate from webpack manifest
precacheAndRoute(self.__WB_MANIFEST, {
  // Ignore all URL parameters.
  ignoreURLParametersMatching: [/.*/] // main.js is loaded with a version hash
});

self.addEventListener('message', event => {
  console.log(`[Message] event: `, event.data);
});
self.addEventListener('install', function(event) {
  event.waitUntil(self.skipWaiting());
});
self.addEventListener('activate', (event) => {
  event.waitUntil(self.clients.claim())
})

addEventListener('message', (event) => {
  console.log("message")
  if (event.data && event.data.type) {
    if (event.data.type === 'GET_VERSION') {
      event.ports[0].postMessage(SW_VERSION);
    }
  }
})

const handler = createHandlerBoundToURL('/index.html');
const navigationRoute = new NavigationRoute(handler);
registerRoute(navigationRoute);

webpack.config.js (shortened for brevity)

const HtmlWebpackPlugin = require('html-webpack-plugin');
const WorkboxPlugin = require('workbox-webpack-plugin');

module.exports = {
  devtool: 'eval-source-map',
  module: {
    rules: [
      {
        test: /\.(jsx?)$/,
        include: [
          path.resolve(__dirname, 'src'),
        ],
        use: ['babel-loader'],
      },
      // [...] (style loading)
      {
        test: /\.html$/,
        use: [
          {
            loader: 'html-loader',
          },
        ],
      },
    ],
  },
  devServer: {
    historyApiFallback: true,
    disableHostCheck: true
  },
  plugins: [
    // [...]
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'src/html/index.html')
    }),
    // [...]
    new WorkboxPlugin.InjectManifest({
      swSrc: './src/sw.js',
      swDest: 'sw.js',
      maximumFileSizeToCacheInBytes: 12 * 1024 * 1024,
    }),
  ],
};

Versions used:

    "webpack": "^4.33.0",
    "webpack-cli": "^3.3.2",
    "workbox-window": "^5.1.3"
    "workbox-webpack-plugin": "^5.1.3"

Checkout this Demo to see the issue (Source)


Solution

  • I was able to solve it myself. The issue was of course that the path to ./sw.js is not correct when the user navigates to a subpath.

    https://my-website.com/a-subpath/a-resource does not resolve ./sw.js to https://my-website.com/sw.js but rather to https://my-website.com/a-subpath/sw.js

    This was a very dumb mistake but hopefully somebody can be helped with this answer. Perhaps it would be helpful if workbox-window didn't resolve correctly as if the registration were a success, but rejected the ./sw.js path.