Search code examples
service-workerworkbox-webpack-pluginaurelia

Access service worker skipWaiting from within App build with Webpack+Workbox


I have a PWA built with Aurelia and compiled with Webpack, using the Workbox Plugin that generates the sw.js service worker file. I'm trying to make the "New version available" user notification so that the user can activate the new version when clicking on a link within the app.

I am successfully downloading and installing the new version in the background, and even detecting that a new version is ready. However, when I try to call the skipWaiting() method to force refresh of the page with the new version, it fails, because apparently I don't have the right scope or object.

The main problem is probably that I can't edit the actual sw.js because it is automatically generated. The examples all suggest the use of self.skipWaiting();, but I don't know how to access that object.

webpack.config.js

new WorkboxPlugin({
  globDirectory: './dist',
  globPatterns: ['**/*.{html,js,css,woff,woff2,ttf,svg,eot,jpg}'],
  swDest: './dist/sw.js',
  clientsClaim: true,
  skipWaiting: false, // because I want to notify the user and wait for response
}),

index.ejs

<script>
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js')
      .then(reg => {
        // make the registration available globally, for access within app
        window.myServiceWorkerReg = reg;
        // Check for update on loading the app (is this necessary?)
        return reg.update();
      })
      .catch(console.error);
  }
</script>

app.js

activate() {
  // listener for service worker update
  this.swReg = window.myServiceWorkerReg;
  console.warn('[app.js] ACTIVATE.', this.swReg);

  this.swReg.addEventListener('updatefound', () => {
    // updated service worker found in reg.installing!
    console.warn('[app.js] UPDATE FOUND.', this.swReg);

    const newWorker = this.swReg.installing;
    newWorker.addEventListener('statechange', () => {
      // has the service worker state changed?
      console.warn('[app.js]  STATE HAS CHANGED.', newWorker, newWorker.state);

      if (newWorker.state === 'installed') {
        // New service worker ready.

        // Notify user; callback for user request to load new app
        myUserMessage({ clickToActivate: () => {
          // reload fresh copy (do not cache)
          console.warn('[app.js] Post Action: skipWaiting.');
          // this.swReg.postMessage({ action: 'skipWaiting' });

          // THIS IS THE LINE THAT FAILS
          this.swReg.skipWaiting();
        }});
      }
    });
  });
}

Everything works fine except the last line (this.swReg.skipWaiting();). Has anyone else used webpack+workbox plugin and gotten the skipWaiting to happen as a result of user interaction?


Solution

  • I finally got it to work. One problem was that I was using an older version of workbox-webpack-plugin. The current version (4.2) includes a listener in the service worker that can trigger self.skipWaiting() when a message is posted to the worker like this:

    newWorker.postMessage({ type: 'SKIP_WAITING' });
    

    But you have to ensure that the config has skipWaiting: false; and that you are using the latest version.

    These instructions are pretty good:

    https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin

    https://developers.google.com/web/tools/workbox/guides/advanced-recipes#offer_a_page_reload_for_users

    However, I tweaked things to work well between my App and the service worker instantiation in the index.ejs file.

    webpack.config.js

    new GenerateSW({
      globPatterns: ['dist/**/*.{html,js,css,woff,woff2,ttf,svg,eot,jpg}'],
      swDest: 'sw.js',
      clientsClaim: true,
      skipWaiting: false,
    })),
    

    index.ejs

    <script>
      if ('serviceWorker' in navigator) {
        // register the service worker
        navigator.serviceWorker.register('/sw.js')
          .then(reg => {
            window.myWorkerReg = reg;
            // Check for update on loading the app (is this necessary?)
            return reg.update();
          })
          .catch(console.error);
        // The event listener that is fired when the service worker updates
        navigator.serviceWorker.addEventListener('controllerchange', function () {
          // when the service worker controller is changed, reload the page
          if (window.swRefreshing) return;
          window.location.reload();
          window.swRefreshing = true;
        });
    
      }
    </script>
    

    app.js

    activate() {
      // listener for service worker update
      this.swReg = window.myWorkerReg;
      if (this.swReg) {
        // if there is already a new service worker ready to install, prompt user
        if (this.swReg.waiting) {
          this.promptUpdateServiceWorker(this.swReg.waiting);
        }
        // add listener to detect when a new service worker is downloaded
        this.swReg.addEventListener('updatefound', () => {
          // updated service worker is being installed
          const newWorker = this.swReg.installing;
          // add listener to detect when installation is finished
          newWorker.addEventListener('statechange', () => {
            if (newWorker.state === 'installed') {
              // New service worker ready to activate; prompt user
              this.promptUpdateServiceWorker(newWorker);
            }
          });
        });
      }
    }
    
    // listener for buildVersion
    buildVersionChanged(buildVersion) {
      // through proprietary code, we've detected a new version could be downloaded now
      window.myWorkerReg.update();
    }
    
    // New service worker ready.  Show the notification
    promptUpdateServiceWorker(newWorker) {
      // actual code for UI prompt will vary; this is pseudocode
      uiPrompt('New_version_ready').then((response) => {
        if (response.approved) {
          // reload fresh copy (do not cache)
          newWorker.postMessage({ type: 'SKIP_WAITING' });
        }
      });
    }