Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recommended Approach for Refreshing Page on new SW #1120

Closed
gauntface opened this issue Dec 13, 2017 · 10 comments
Closed

Recommended Approach for Refreshing Page on new SW #1120

gauntface opened this issue Dec 13, 2017 · 10 comments

Comments

@gauntface
Copy link

gauntface commented Dec 13, 2017

@beatrizdemiguelperez asked this question in the Slack channel and I know the twitter bootstrap peeps had similar issues so would be good to add some documentation on the topic.

I've just been looking around the old Chrome Dev Summit site and kind of pulled together this solution which detects the update and will refresh all clients, but I know @jeffposnick is a wizard with this, so wanted to check in in-case he has a better approach and cc @jakearchibald as well.

Approach that seems to work for me is:

In the window

const showRefreshUI = (registration) => {
  const container = document.querySelector('.new-sw');
  container.style.display = 'block';
  
  const button = document.querySelector('button');
  button.addEventListener('click', () => {
    button.disabled = true;
    
    registration.waiting.postMessage('force-activate');
  });
};

const onNewServiceWorker = (registration, callback) => {
  if (registration.waiting) {
    // SW is waiting to activate. Can occur if multiple clients open and
    // one of the clients is refreshed.
    return callback();
  }

  const listenInstalledStateChange = () => {
    registration.installing.addEventListener('statechange', () => {
      if (event.target.state === 'installed') {
        // A new service worker is available, inform the user
        callback();
      }
    });
  };

  if (registration.installing) {
    return listenInstalledStateChange();
  }

  // We are currently controlled so a new SW may be found...
  // Add a listener in case a new SW is found,
  registration.addEventListener('updatefound', listenInstalledStateChange);
}

window.addEventListener('load', () => {
  // When the user asks to refresh the UI, we'll need to reload the window
  navigator.serviceWorker.addEventListener('message', (event) => {
    if (!event.data) {
      return;
    }
    
    switch (event.data) {
      case 'reload-window':
        window.location.reload();
        break;
      default:
        // NOOP
        break;
    }
  });
  
  navigator.serviceWorker.register('/sw.js')
  .then(function (registration) {
      // Track updates to the Service Worker.
    if (!navigator.serviceWorker.controller) {
      // The window client isn't currently controlled so it's a new service
      // worker that will activate immediately
      return;
    }

    onNewServiceWorker(registration, () => {
      showRefreshUI(registration);  
    });
  });
});

In the service worker

self.addEventListener('message', (event) => {
  if (!event.data){
    return;
  }
  
  switch (event.data) {
    case 'force-activate':
      self.skipWaiting();
      self.clients.claim();
      self.clients.matchAll().then((clients) => {
        clients.forEach((client) => client.postMessage('reload-window'));
      });
      break;
    default:
      // NOOP
      break;
  }
});

The way this works is that when ever a "waiting" service worker is found, we call showRefreshOption() which would show the UI to the user and when the user clicks on the refresh UI, we add a listener to wait for a controller change and post a message to the new service worker. The service worker skips waiting, and claims current clients.

This seems to be safe with multiple clients open and being controlled by a service worker and it plays nice with the DevTools update on reload feature.

Glitch: https://glitch.com/edit/#!/sassy-eel?path=views/index.html:86:3

@jeffposnick
Copy link
Contributor

I mentioned this in google/WebFundamentals#5493 as well, but I really like the writeup that @dfabulich from the Redfin team put together at https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68 and I feel like pointing folks to that right now could end up being more useful that providing our or copy-and-paste solution.

I've brought this up already in meetings, but we don't have an issue tracking it so perhaps we could reuse this issue: we can add some value by creating a new Workbox module that runs in the browser and elevates this code from a copy-and-paste recipe to a drop-in solution. We can paper over some of the code decisions that a developer would normally have to make by exposing configuration options with sensible defaults.

This would require design docs and thinking about the interface, and would be in a post-v3 timeframe.

@gauntface
Copy link
Author

To be honest - the article is full of valuable info, but pointing people to that is horrendous in terms of the knowledge required to be learnt when all they want is to add a refresh button, providing a copy and paste solution is a valid one here.

I totally agree that we should look into adding a window API, but I think that will happen in the new year when we plan out v4 features and start design docs etc.

@gauntface
Copy link
Author

This has landed on WebFundamentals, so going to close this for now.

@dfabulich
Copy link

Thanks for linking to my guide! I just filed google/WebFundamentals#5504 on this… I don't agree with the guidance there.

Overall, I feel that Workbox could stand to do a little more here… at the very least, Workbox could/should automatically generate a message handler that can handle the force-activate messages that the documentation recommends.

@dfabulich
Copy link

As for @jeffposnick's suggestion that Workbox could offer a page-client-side solution here, I think the right thing to do is to provide a "prolyfill" (a forward-looking polyfill for behavior that may one day be standardized).

I've posted a suggested prolyfill in a comment to w3c/ServiceWorker#1222 and I think it would be better to just standardize on that.

@beatrizdemiguelperez
Copy link

I tried this approach and it works. I think the docs are a little bit confusing. postMessage works but "controllerchange" event don't (or I cannot get this working)
Thank you for the suggestions :)

@gauntface
Copy link
Author

@beatrizdemiguelperez Will take a look again. What can I change in the docs to make it less confusing?

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

@dfabulich
Copy link

Perhaps a working example would help? I just made this. https://github.com/dfabulich/service-worker-refresh-sample

@beatrizdemiguelperez Does the sample work for you?

@beatrizdemiguelperez
Copy link

beatrizdemiguelperez commented Dec 18, 2017

Well, I don't think that this is an "advanced recipe".

I mean, If you want to use workbox to precache your files, you have to be sure about it when you deploy your web because there's no going back.

You always want all your clients to update the web, as you did with App Cache.
If you make a mistake here, users will see an outdated version.

So for me, It is very important to know exactly when a serviceWorker is waiting and when a serviceWorker is activated and when you have to reload to get the new changes.
I think that those events should be described at the beginning.

If you understand how to capture these events I think anything you want to do is easy.

Of course, UX is important and showing a prompt when sw is waiting would be nice (actually I would add a gif to be more clear and show this common pattern) but maybe you only want to listen to the event and force update.
I mean, it's flexible.

I have been working on this ember-cli addon .
I created an evented Ember.Service so you can subscribe to:

  • registrationComplete: sw successfully registered
  • registrationError: sw not registered
  • newSWActive: new sw controlling page
  • newSWWaiting: new sw waiting for controlling page
  • unregistrationComplete: all sw are unregistered

I got this working sending a message to serviceWorker 'force-activate' and then on sw sending to clients a message 'reload-window'.
I call workboxBuild.generateSW with importScripts: ['skip-waiting.js'] param where skip-waiting.js has:

self.addEventListener('message', ({ data }) => {
	if (data === 'force-activate') {
		self.skipWaiting();
		self.clients.claim();
		self.clients.matchAll().then((clients) => {
			clients.forEach((client) => client.postMessage('reload-window'));
		});
	}
});

The 'controllerchange' event didn't work for me. :(

What I meant when i said that the docs were not clear is because if you search you have many ways and blogs about how to capture these events and It's a little crazy.
I want only one way to do this and I don't want to read so many docs because for me it shouldn't be that complicated, Its only an event.

I understand that workbox is about how to cache but if you don't understand serviceWorker lifecycle also you can't/shouldn't use workbox.

(Sorry if my english is not that perfect haha)

Thanks again

@dfabulich
Copy link

@beatrizdemiguelperez

  1. I agree completely with this:

if you search you have many ways and blogs about how to capture these events and It's a little crazy.

I'd appreciate it if you would make your voice heard on w3c/ServiceWorker#1222 and/or w3c/ServiceWorker#1247 where we're discussing modifying the standard to make this code easier to manage.

In w3c/ServiceWorker#1222 I propose a one-line way of detecting whether a service worker is waiting with a .waiting promise on navigator.serviceWorker.

navigator.serviceWorker.waiting.then(reg => {
  if (confirm('refresh now?')) reg.waiting.postMessage('skipWaiting');
});

Jake counter-proposed w3c/ServiceWorker#1247 which would make it just slightly easier to listen to the statechange event, by having it bubble up to the registration.

Please comment on those issues to say which approach you would prefer. (I fear that the spec editors think that I'm the only one complaining about this!)

  1. You don't say what happened when controllerchange "didn't work." What did you expect to happen? What actually happened? Can you provide sample code that we can use to reproduce your issue?

  2. Does the sample work for you? https://github.com/dfabulich/service-worker-refresh-sample

  3. I may be able to guess what your controllerchange problem was: when when using the current version of the WorkBox controllerchange recipe, there's a bug where it goes into an infinite refresh loop if you're using Chrome Dev Tools with the "Update on Reload" box checked on the Application tab.

I've submitted a pull request to work around that issue: google/WebFundamentals#5515

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants