乐闻世界logo
搜索文章和话题

How to activate updated service worker on refresh

8 个月前提问
5 个月前修改
浏览次数91

5个答案

1
2
3
4
5

在页面刷新时激活更新后的service worker,通常涉及以下几个步骤:

  1. 注册Service Worker: 首先,需要在你的网页中注册service worker。这通常在JavaScript的主文件中进行:

    javascript
    if ('serviceWorker' in navigator) { window.addEventListener('load', function() { navigator.serviceWorker.register('/service-worker.js').then(function(registration) { // 注册成功 console.log('ServiceWorker registration successful with scope: ', registration.scope); }, function(err) { // 注册失败 console.log('ServiceWorker registration failed: ', err); }); }); }
  2. 更新Service Worker文件: 当你更新了service worker的JavaScript文件(service-worker.js),浏览器会检测到文件内容的变化,这时候新的service worker会开始安装流程,但此时不会立即激活。

  3. Install和Activate事件: 在service worker文件内部,你可以监听installactivate事件。新的service worker在安装后通常会进入等待状态,直到所有的客户端(页面)都关闭,然后才会被激活。

    javascript
    self.addEventListener('install', event => { // 执行安装步骤 }); self.addEventListener('activate', event => { // 在激活时,通常会清理旧的缓存等 event.waitUntil( // 清理工作 ); });
  4. 立即激活新的Service Worker: 若要在页面刷新时立即激活新的service worker,可以利用self.skipWaiting()方法。在install事件中调用这个方法可以使新的service worker跳过等待阶段,直接进入激活状态。

    javascript
    self.addEventListener('install', event => { event.waitUntil( // 安装步骤例如缓存文件等, // 然后调用skipWaiting来立即激活service worker self.skipWaiting() ); });
  5. 控制页面: 即使service worker已经激活,如果页面在安装新的service worker之前就已经打开了,那么你还需要通过clients.claim()在activate事件中获取对其控制权。

    javascript
    self.addEventListener('activate', event => { event.waitUntil( clients.claim() // 这个操作会使新的service worker立即控制所有的页面 ); });
  6. 页面刷新: 你可以在页面上提供一个机制来刷新页面,或者通过service worker通知用户,并使用window.location.reload()来刷新页面以使用更新后的service worker。

    javascript
    navigator.serviceWorker.addEventListener('controllerchange', () => { window.location.reload(); });
  7. 确保更新的Service Worker被应用: 对于已经打开的页面,为了立即应用新的service worker,你可能需要提醒用户刷新页面,或者使用前面提到的window.location.reload()强制刷新。

通过以上步骤,页面刷新后,更新后的service worker可以被激活并立即开始控制页面。不过,请注意,强制刷新用户的页面可能会导致用户体验不佳,因此应当谨慎使用。

2024年6月29日 12:07 回复

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:

  1. Serve page shell, css & js from a cache using the service worker.
  2. Populate page with content from the cache.
  3. Attempt to fetch content from the network.
  4. 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.

  1. Detect updated service worker "waiting"
  2. Show notification to user "Update available - update now"
  3. When the user clicks this notification, postmessage to the service worker, asking it to call skipWaiting.
  4. Detect the new service worker becoming "active"
  5. window.location.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.

2024年6月29日 12:07 回复

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:

  1. skipWaiting() on installation. This is dangerous, because your tab can get a mix of old and new content.
  2. skipWaiting() on installation, and refresh all open tabs on controllerchange Better, but your users may be surprised when the tab randomly refreshes.
  3. 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.
  4. 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:

shell
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:

shell
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);
2024年6月29日 12:07 回复

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.

Note: if you are using a recent version of react-scripts, the snippet below is automatically added and you won't need to use cra-append-sw.

shell
self.addEventListener('message', event => { if (!event.data) { return; } if (event.data === 'SKIP_WAITING') { 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:

shell
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('SKIP_WAITING'); }; } 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.

shell
.catch(error => { console.error('Error during service worker registration:', error); }); navigator.serviceWorker.addEventListener('controllerchange', () => { if (refreshing) { return; } refreshing = true; window.location.reload(); });
2024年6月29日 12:07 回复

Even though the OP has disagreed with skipWaiting, skipWaiting is actually the correct solution for many apps, especially single page apps.

Open your precache in DevTools, check if there are any lazy loaded files. If there aren't - meaning everything in your precache are fetched immediately on page load, then it's safe to call skipWaiting, which will delete the old precache, replace with new assets (including the new index.html), waiting to be served on the next refresh. Since all the old assets are already in memory, which are running with the old index.html, there's nothing wo worry about.

(If you see a file in the precache but you aren't sure whether it's downloaded on page load, reload and watch the network traffic in DevTools yourself)

If you do have lazy loaded assets, then it indeed could cause a problem, but that's not a new problem created by service worker. Imagine your user opened a tab, then you deploy a new version, then the user clicks something which triggers a lazy load, but that asset is no longer on your server. This is an old problem. Precache unintentionally solved this problem, but you shouldn't rely on it, instead you should have your own exception handling logic, unless your app was designed to leverage the precache from the very beginning.

2024年6月29日 12:07 回复

你的答案