可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
When a service worker updates, it doesn't take control of the page right way; it goes into a "waiting" state, waiting to be activated.
Surprisingly, the updated service worker doesn't even take control of the tab after refreshing the page. Google explains:
https://developers.google.com/web/fundamentals/instant-and-offline/service-worker/lifecycle
Even if you only have one tab open to the demo, refreshing the page
isn't enough to let the new version take over. This is due to how
browser navigations work. When you navigate, the current page doesn't
go away until the response headers have been received, and even then
the current page may stay if the response has a Content-Disposition
header. Because of this overlap, the current service worker is always
controlling a client during a refresh.
To get the update, close or navigate away from all tabs using the
current service worker. Then, when you navigate to the demo again, you
should see the horse [updated content].
This pattern is similar to how Chrome updates. Updates to Chrome
download in the background, but don't apply until Chrome restarts. In
the mean time, you can continue to use the current version without
disruption. However, this is a pain during development, but DevTools
has ways to make it easier, which I'll cover later in this article.
This behavior makes sense in the case of multiple tabs. My app is a bundle that needs to be updated atomically; we can't mix and match part of the old bundle and part of the new bundle. (In a native app, atomicity is automatic and guaranteed.)
But in the case of a single tab, which I'll call the "last open tab" of the app, this behavior is not what I want. I want refreshing the last open tab to update my service worker.
(It's hard to imagine that anybody actually wants the old service worker to continue running when the last open tab is refreshed. Google's "navigation overlap" argument sounds to me like a good excuse for an unfortunate bug.)
My app is normally only used in a single tab. In production, I want my users to be able to use the latest code just by refreshing the page, but that won't work: the old service worker will remain in control across refreshes.
I don't want to have to tell users, "to receive updates, be sure to close or navigate away from my app." I want to tell them, "just refresh."
How can I activate my updated service worker when the user refreshes the page?
EDIT: There's one answer I'm aware of that's quick, easy, and wrong: skipWaiting
in the service worker's install event. skipWaiting
will make the new service worker take effect as soon as the update downloads, while the old page tab is open. That makes the update unsafely nonatomic; it's like replacing a native app bundle while the app's running. That's not OK for me. I need to wait until the user refreshes the page of the last open tab.
回答1:
There are conceptually two kinds of updates, and it isn't clear from your question which we're talking about, so I'll cover both.
Updating content
Eg:
- The text of a blog post
- The schedule for an event
- A social media timeline
These will likely be stored in the cache API or IndexedDB. These stores live independently of the service worker - updating the service worker shouldn't delete them, and updating them shouldn't require an update to the service worker
Updating the service worker is the native equivalent of shipping a new binary, you shouldn't need to do that to (for example) update an article.
When you update these caches is entirely up to you, and they aren't updated without you updating them. I cover a number of patterns in the offline cookbook, but this is my favourite one:
- Serve page shell, css & js from a cache using the service worker.
- Populate page with content from the cache.
- Attempt to fetch content from the network.
- If the content is fresher than what you have in the cache, update the cache and the page.
In terms of "update the page", you need to do that in a way that isn't disruptive to the user. For chronological lists this is pretty easy, as you just add the new stuff to the bottom/top. If it's updating an article it's a bit trickier, as you don't want to switch text from underneath the user's eyes. In that case it's often easier to show some kind of notification like "Update available - show update" which, when clicked, refreshes the content.
Trained to thrill (perhaps the first ever service worker example) demonstrates how to update a timeline of data.
Updating the "app"
This is the case where you do want to update the service worker. This pattern is used when you're:
- Updating your page shell
- Updating JS and/or CSS
If you want the user to get this update in a non-atomic, but fairly safe way, there are patterns for this too.
- Detect updated service worker "waiting"
- Show notification to user "Update available - update now"
- When the user clicks this notification, postmessage to the service worker, asking it to call
skipWaiting
.
- Detect the new service worker becoming "active"
window.loaction.reload()
Implementing this pattern is part of my service worker Udacity course, and here's the diff of the code before/after.
You can take this further too. For example, you could employ some kind of semver-esque system, so you know the version of the service worker the user currently has. If the update is minor you may decide calling skipWaiting()
is totally safe.
回答2:
I wrote a blog post explaining how to handle this. https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68
I also have a working sample on github. https://github.com/dfabulich/service-worker-refresh-sample
There are four approaches:
skipWaiting()
on installation. This is dangerous, because your tab can get a mix of old and new content.
skipWaiting()
on installation, and refresh all open tabs on controllerchange
Better, but your users may be surprised when the tab randomly refreshes.
- Refresh all open tabs on
controllerchange
; on installation, prompt the user to skipWaiting()
with an in-app "refresh" button. Even better, but the user has to use the in-app "refresh" button; the browser's refresh button still won't work. This is documented in detail in Google's Udacity course on Service Workers and the Workbox Advanced Guide, as well as the master
branch of my sample on Github.
- Refresh the last tab if possible. During fetches in
navigate
mode, count open tabs ("clients"). If there's just one open tab, then skipWaiting()
immediately and refresh the only tab by returning a blank response with a Refresh: 0
header. You can see a working example of this in the refresh-last-tab
branch of my sample on Github.
Putting it all together...
In the Service Worker:
addEventListener('message', messageEvent => {
if (messageEvent.data === 'skipWaiting') return skipWaiting();
});
addEventListener('fetch', event => {
event.respondWith((async () => {
if (event.request.mode === "navigate" &&
event.request.method === "GET" &&
registration.waiting &&
(await clients.matchAll()).length < 2
) {
registration.waiting.postMessage('skipWaiting');
return new Response("", {headers: {"Refresh": "0"}});
}
return await caches.match(event.request) ||
fetch(event.request);
})());
});
In the page:
function listenForWaitingServiceWorker(reg, callback) {
function awaitStateChange() {
reg.installing.addEventListener('statechange', function() {
if (this.state === 'installed') callback(reg);
});
}
if (!reg) return;
if (reg.waiting) return callback(reg);
if (reg.installing) awaitStateChange();
reg.addEventListener('updatefound', awaitStateChange);
}
// reload once when the new Service Worker starts activating
var refreshing;
navigator.serviceWorker.addEventListener('controllerchange',
function() {
if (refreshing) return;
refreshing = true;
window.location.reload();
}
);
function promptUserToRefresh(reg) {
// this is just an example
// don't use window.confirm in real life; it's terrible
if (window.confirm("New version available! OK to refresh?")) {
reg.waiting.postMessage('skipWaiting');
}
}
listenForWaitingServiceWorker(reg, promptUserToRefresh);
回答3:
In addition to Dan's answer;
1. Add the skipWaiting
event listener in the service worker script.
In my case, a CRA based application I use cra-append-sw to add this snippet to the service worker.
self.addEventListener('message', event => {
if (!event.data) {
return;
}
if (event.data === 'skipWaiting') {
self.skipWaiting();
}
});
2. Update register-service-worker.js
For me, it felt natural to reload the page when the user navigates within the site. In the register-service-worker script I've added the following code:
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
const pushState = window.history.pushState;
window.history.pushState = function () {
// make sure that the user lands on the "next" page
pushState.apply(window.history, arguments);
// makes the new service worker active
installingWorker.postMessage('skipWaiting');
};
} else {
Essentially we hook into the native pushState
function so we know that the user is navigating to a new page. Although, if you also use filters in your URL for a products page, for example, you may want to prevent reloading the page and wait for a better opportunity.
Next, I'm also using Dan's snippet to reload all tabs when the service worker controller changes. This also will reload the active tab.
.catch(error => {
console.error('Error during service worker registration:', error);
});
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) {
return;
}
refreshing = true;
window.location.reload();
});