web-dev-qa-db-ja.com

更新時に更新されたService Workerをアクティブ化する

Service Workerが更新されると、ページを適切に制御できません。 「待機」状態になり、アクティブ化されるのを待機します。

驚いたことに、更新されたサービスワーカーは、ページを更新した後もタブを制御しません。 Googleの説明:

https://developers.google.com/web/fundamentals/instant-and-offline/service-worker/lifecycle

デモで開いているタブが1つしかない場合でも、ページを更新するだけでは、新しいバージョンに引き継ぐことはできません。これは、ブラウザナビゲーションの仕組みによるものです。ナビゲートすると、応答ヘッダーが受信されるまで現在のページは消えず、応答にContent-Dispositionヘッダーがある場合でも、現在のページが残る場合があります。このオーバーラップのため、現在のサービスワーカーは更新中に常にクライアントを制御しています。

更新を取得するには、現在のService Workerを使用してすべてのタブを閉じるか、すべてのタブから移動します。その後、再びデモに移動すると、馬[更新されたコンテンツ]が表示されます。

このパターンは、Chromeの更新方法に似ています。 Chromeの更新はバックグラウンドでダウンロードされますが、Chromeが再起動するまで適用されません。それまでの間、中断することなく現在のバージョンを引き続き使用できます。ただし、これは開発中の苦痛ですが、DevToolsにはそれを簡単にする方法があります。これについては、この記事の後半で説明します。

この動作は、複数のタブの場合に理にかなっています。私のアプリは、アトミックに更新する必要があるバンドルです。古いバンドルの一部と新しいバンドルの一部を混在させて一致させることはできません。 (ネイティブアプリでは、原子性は自動的に保証されます。)

しかし、アプリの「最後に開いたタブ」と呼ぶ単一のタブの場合、この動作は私が望むものではありません。最後に開いたタブを更新して、サービスワーカーを更新したい。

(実際に誰かがを最後に開いたタブが更新されたときに古いサービスワーカーが実行を継続することを想像するのは困難です。私は不幸なバグの良い言い訳が好きです。)

私のアプリは通常、単一のタブでのみ使用されます。本番環境では、ページを更新するだけでユーザーが最新のコードを使用できるようにしたいと思いますが、それは機能しません。古いサービスワーカーが更新後も制御を維持します。

「更新プログラムを受信するには、必ずアプリを閉じるか、アプリから移動してください」とユーザーに伝える必要はありません。私は彼らに「ただリフレッシュしてください」と伝えたいです。

ユーザーがページを更新したときに、更新されたService Workerをアクティブにするにはどうすればよいですか?

編集:私はそれが迅速、簡単、そして間違っていることを知っている1つの答えがあります:Service WorkerのインストールイベントのskipWaitingskipWaitingは、古いページのタブが開いている間に、更新がダウンロードされるとすぐに新しいService Workerを有効にします。これにより、更新は安全ではない非アトミックになります。アプリの実行中にネイティブアプリバンドルを置き換えるようなものです。私には大丈夫ではありません。ユーザーが最後に開いたタブのページを更新するまで待つ必要があります。

21
Dan Fabulich

これを処理する方法を説明するブログ記事を書きました。 https://redfin.engineering/how-to-fix-the-refresh-button-when-using-service-workers-a8e27af6df68

Githubにも実用的なサンプルがあります。 https://github.com/dfabulich/service-worker-refresh-sample

4つのアプローチがあります。

  1. skipWaiting()インストール時。これは危険なことです。なぜなら、タブには古いコンテンツと新しいコンテンツが混在する可能性があるからです。
  2. skipWaiting()インストール時に、開いているすべてのタブをcontrollerchangeで更新します。しかし、ユーザーは、タブがランダムに更新されます。
  3. controllerchangeで開いているすべてのタブを更新します。インストール時に、アプリ内の「更新」ボタンでskipWaiting()にユーザーにプロンプ​​トを表示します。さらに良いことですが、ユーザーはアプリ内の「更新」ボタンを使用する必要があります。 ;ブラウザの更新ボタンはまだ機能しません。これは Googleのサービスワーカーに関するUdacityコース および Workbox上級ガイド に詳細に記載されています。また、私の サンプルのmasterブランチGithubで
  4. 可能であれば最後のタブを更新します。navigateモードでのフェッチ中に、開いているタブ(「クライアント」)をカウントします。開いているタブが1つだけの場合は、すぐにskipWaiting()し、Refresh: 0ヘッダーを含む空白の応答を返すことで、唯一のタブを更新します。この実例は、私の Githubのサンプルrefresh-last-tabブランチで見ることができます。

すべてを一緒に入れて...

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);
  })());
});

ページ内:

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);
14
Dan Fabulich

概念的には2種類の更新がありますが、あなたの質問から私たちが話していることは明らかではないので、両方を取り上げます。

コンテンツを更新する

例えば:

  • ブログ投稿のテキスト
  • イベントのスケジュール
  • ソーシャルメディアのタイムライン

これらは、キャッシュAPIまたはIndexedDBに保存される可能性があります。これらのストアはService Workerとは独立して存在します。ServiceWorkerを更新しても削除されず、Service Workerを更新する必要はありません。

Service Workerの更新は、新しいバイナリを出荷するのにネイティブに相当します。記事を更新するためにこれを行う必要はありません。

これらのキャッシュを更新するのは完全にあなた次第であり、更新しない限り更新されません。 オフラインクックブック で多くのパターンを扱っていますが、 これは私のお気に入りです

  1. サービスワーカーを使用して、キャッシュからページシェル、CSS、JSを提供します。
  2. ページをキャッシュのコンテンツで埋めます。
  3. ネットワークからコンテンツを取得しようとします。
  4. コンテンツがキャッシュにあるものよりも新しい場合は、キャッシュとページを更新します。

「ページの更新」に関しては、ユーザーを混乱させない方法でそれを行う必要があります。年代順リストの場合、新しいものを下/上に追加するだけなので、これは非常に簡単です。記事を更新する場合、ユーザーの目の下からテキストを切り替えたくないため、少し注意が必要です。その場合、「Update available-show update」などの種類の通知を表示する方が簡単で、クリックするとコンテンツが更新されます。

スリルに訓練された (おそらく最初のサービスワーカーの例)は、データのタイムラインを更新する方法を示しています。

「アプリ」の更新

これは、Service Workerを更新するdoの場合です。このパターンは、次の場合に使用されます。

  • ページシェルの更新
  • JSおよび/またはCSSの更新

ユーザーにこの更新を原子的ではないがかなり安全な方法で取得させたい場合は、このパターンもあります。

  1. 更新されたService Workerの「待機中」を検出
  2. ユーザーに通知を表示「更新が利用可能-今すぐ更新
  3. ユーザーがこの通知をクリックすると、サービスワーカーにメッセージをポストし、skipWaitingの呼び出しを求めます。
  4. 「アクティブ」になる新しいサービスワーカーを検出する
  5. window.loaction.reload()

このパターンを実装することは、私の サービスワーカーUdacityコース の一部であり、 コードの前後の差分 です。

これをさらに進めることもできます。たとえば、何らかの種類のsemver風のシステムを採用して、ユーザーが現在所有しているService Workerのバージョンを把握できます。更新がマイナーな場合、skipWaiting()を呼び出すことは完全に安全です。

31
JaffaTheCake

ダンの答えに加えて。

1。 Service WorkerスクリプトにskipWaitingイベントリスナーを追加します。

私の場合、CRAベースのアプリケーション cra-append-sw を使用して、このスニペットをService Workerに追加します。

self.addEventListener('message', event => {
  if (!event.data) {
    return;
  }

  if (event.data === 'skipWaiting') {
    self.skipWaiting();
  }
});

2。 register-service-worker.jsを更新します

私にとって、ユーザーがサイト内を移動したときにページをリロードするのは自然なことでした。 register-service-workerスクリプトに、次のコードを追加しました。

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 {

基本的に、ネイティブのpushState関数にフックして、ユーザーが新しいページに移動していることを確認します。ただし、たとえば、製品ページのURLでフィルターを使用する場合は、ページの再読み込みを防ぎ、より良い機会を待つことができます。

次に、Danのスニペットを使用して、Service Workerコントローラーが変更されたときにすべてのタブを再読み込みします。これにより、アクティブなタブもリロードされます。

  .catch(error => {
    console.error('Error during service worker registration:', error);
  });

navigator.serviceWorker.addEventListener('controllerchange', () => {
  if (refreshing) {
    return;
  }

  refreshing = true;
  window.location.reload();
});
3
Christiaan