简体   繁体   中英

Register service worker after hard refresh

Whenever the web application with service worker is reloaded after hard refresh ( Ctrl + F5 for Chrome, for example), the service worker is unable to register afterwards.

According to the documentation at W3C

Note: navigator.serviceWorker.controller returns null if the request is a force refresh (shift+refresh). The ServiceWorker objects returned from this attribute getter that represent the same service worker are the same objects.

So, the question is: is it really impossible to register service worker right after the hard refresh is performed? I am checking the existence of the service worker with the navigator.serviceWorker.controller . Please see the attached GIF which shows interaction with the https://googlechrome.github.io/samples/service-worker/basic/ page

在此处输入图片说明

this is a very complicated story, the long road here is:

This is actually a spec of Service Worker. And only present in recent change of Chrome. For the earlier version of Chrome , Service Worker has no any issue surviving a "force refresh".

Meaning, that only in the recent change of Chrome, the problem persisted. In earlier version it did not persist, what hints about a problem with chrome. This quote was up to the year of 2016 - All versions after this will suffer the same effect , only earlier versions will not present the issue.

there are developers that are suggesting that force refresh or hard refresh should always clear out all kind of caches . Which is matched with the purpose of its existent and its spec - however the match I personally believe it's debatable .


the solution:

You will be able to do it using js plugins which detect the key hit of Ctrl or Shift... then prevent the "force refresh" to happen.

This is so far the best an easiest solution to handle the issue.

It seems pretty hard to prevent the hard refresh, but you can detect that a worker should be used but isn't currently. Then you can reload using it.

navigator.serviceWorker.getRegistration().then(function(reg) {
  // There's an active SW, but no controller for this tab.
  if (reg.active && !navigator.serviceWorker.controller) {
    // Perform a soft reload to load everything from the SW and get
    // a consistent set of resources.
    window.location.reload();
  }
});

Workaround solution for making service workers work even after hard reload is by unregistering and registering the service worker scripts through the code snippet below.

   if ('serviceWorker' in navigator) {
      if (navigator.serviceWorker.controller) {
        navigator.serviceWorker.getRegistration(navigator.serviceWorker.controller.scriptURL).then(function (sw) {
          if (sw) {
            sw.unregister().then(() => {
              navigator.serviceWorker.register('service-worker.js');
            });
          }
        });
      } else {
        const url = window.location.protocol + '//' + window.location.host + '/service-worker.js';
        navigator.serviceWorker.getRegistration(url).then(function (sw) {
          if (sw) {
            sw.unregister().then(() => {
              navigator.serviceWorker.register('service-worker.js');
            });
          }
        });
      }
    }

The answer by @Durim was on the right track... here for reference (and indubitably I'll find it again the next time I need to do this) is a full solution combining every technique, every check, such that when the promise returns you can be sure (barring outside interference) that the service worker is available, hard reload or not:

export default async function registerServiceWorker(tryOnce = false) {
    if (!('serviceWorker' in navigator)) throw new Error('serviceWorker not supported');

    const url = (new URL(`/http/script/_sw.js?hash=${swhash}`, location)).toString();
    console.info('Registering worker');
    const registration = await navigator.serviceWorker.register(url, {
        scope: '/',
    });

    const registeredWorker = registration.active || registration.waiting || registration.installing;
    console.info('Registered worker:', registeredWorker);
    if (registeredWorker?.scriptURL != url) {
        console.log('[ServiceWorker] Old URL:', registeredWorker?.scriptURL || 'none', 'updating to:', url);
        await registration.update();
        console.info('Updated worker');
    }

    console.info('Waiting for ready worker');
    let serviceReg = await navigator.serviceWorker.ready;
    console.info('Ready registration:', serviceReg);

    if (!navigator.serviceWorker.controller) {
        console.info('Worker isn’t controlling, re-register');
        try {
            const reg = await navigator.serviceWorker.getRegistration('/');
            console.info('Unregistering worker');
            await reg.unregister();
            console.info('Successfully unregistered, trying registration again');
            return registerServiceWorker();
        } catch (err) {
            console.error(`ServiceWorker failed to re-register after hard-refresh, reloading the page!`, err);
            return location.reload();
        }
    }

    let serviceWorker = serviceReg.active || serviceReg.waiting || serviceReg.installing;
    if (!serviceWorker) {
        console.info('No worker on registration, getting registration again');
        serviceReg = await navigator.serviceWorker.getRegistration('/');
        serviceWorker = serviceReg.active || serviceReg.waiting || serviceReg.installing;
    }

    if (!serviceWorker) {
        console.info('No worker on registration, waiting 50ms');
        await sleep(50); // adjustable or skippable, have a play around
    }

    serviceWorker = serviceReg.active || serviceReg.waiting || serviceReg.installing;
    if (!serviceWorker) throw new Error('after waiting on .ready, still no worker');

    if (serviceWorker.state == 'redundant') {
        console.info('Worker is redundant, trying again');
        return registerServiceWorker();
    }

    if (serviceWorker.state != 'activated') {
        console.info('Worker IS controlling, but not active yet, waiting on event. state=', serviceWorker.state);
        try {
            // timeout is adjustable, but you do want one in case the statechange
            // doesn't fire / with the wrong state because it gets queued,
            // see ServiceWorker.onstatechange MDN docs.
            await timeout(100, new Promise((resolve) => {
                serviceWorker.addEventListener('statechange', (e) => {
                    if (e.target.state == 'activated') resolve();
                });
            }));
        } catch (err) {
            if (err instanceof TimeoutError) {
                if (serviceWorker.state != 'activated') {
                    if (tryOnce) {
                        console.info('Worker is still not active. state=', serviceWorker.state);
                        throw new Error('failed to activate service worker');
                    } else {
                        console.info('Worker is still not active, retrying once');
                        return registerServiceWorker(true);
                    }
                }
            } else {
                // should be unreachable
                throw err;
            }
        }
    }

    console.info('Worker is controlling and active, we’re good folks!');
    return serviceWorker;
}

export class TimeoutError extends Error { }

/**
 * Run promise but reject after some timeout.
 *
 * @template T
 * @param {number} ms Milliseconds until timing out
 * @param {Promise<T>} promise Promise to run until timeout (note that it will keep running after timeout)
 * @returns {Promise<T, Error>}
 */
export function timeout(ms, promise) {
    return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
            reject(new TimeoutError);
        }, ms);

        promise.then((result) => {
            clearTimeout(timer);
            resolve(result);
        }, (error) => {
            clearTimeout(timer);
            reject(error);
        });
    })
}

All the console.info statements are there for your debugging pleasure and should probably be removed in production.

Obviously you'll want to change the /http/script/_sw.js?hash=${swhash} part (the URL of the service worker). Note a further technique here where the URL contains the hash (provided externally) of the service worker file for updates; this might not be appropriate for your use-case, make it your own.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM