バックグラウンド取得の概要

Jake Archibald 氏
Jake Archibald

2015 年に、Service Worker はユーザーが接続されるまで作業を遅らせることができるバックグラウンド同期が導入されました。つまり、ユーザーはメッセージを入力して [送信] を押すと、メッセージをすぐにまたは接続したときにメッセージが送信されることを認識して、サイトを離れる可能性があります。

これは便利な機能ですが、取得中に Service Worker が動作している必要があります。メッセージの送信などの短い処理では問題になりませんが、タスクに時間がかかりすぎると、ブラウザが Service Worker を強制終了します。そうしないと、ユーザーのプライバシーとバッテリーが危険にさらされます。

映画、ポッドキャスト、ゲームのレベルなど、時間がかかるものをダウンロードする必要がある場合はどうすればよいでしょうか。これがバックグラウンド取得の役割です。

バックグラウンド取得は、Chrome 74 以降、デフォルトで使用できます。

以下の 2 分間の簡単なデモでは、バックグラウンド フェッチを使用した場合と、従来の状態の違いを説明します。

ご自身でデモを試してコードを確認してください。

仕組み

バックグラウンド フェッチの仕組みは次のとおりです。

  1. 複数のフェッチをバックグラウンドで実行するようにブラウザに指示します。
  2. ブラウザはこれらを取得して進行状況をユーザーに表示します。
  3. 取得が完了または失敗すると、ブラウザが Service Worker を開き、何が起こったかを知らせるイベントを発生させます。ここで、レスポンスの処理方法を決定します。

ステップ 1 の後にユーザーがサイトのページを閉じても、ダウンロードは続行されます。フェッチは見やすく、簡単に中止できるため、バックグラウンド同期タスクが長すぎるというプライバシーに関する懸念はありません。Service Worker は常に実行されているわけではないため、バックグラウンドでビットコイン マイニングを行うなど、システムを悪用するおそれはありません。

一部のプラットフォーム(Android など)では、ブラウザがオペレーティング システムにフェッチを渡せるため、ステップ 1 の後にブラウザを閉じることができます。

ユーザーがオフライン中にダウンロードを開始した場合、またはダウンロード中にオフラインになった場合、バックグラウンドの取得は一時停止され、後で再開されます。

API

特徴検出

他の新機能と同様に、ブラウザがそれをサポートしているかどうかを確かめる必要があります。バックグラウンド フェッチの場合、次のように簡単です。

if ('BackgroundFetchManager' in self) {
  // This browser supports Background Fetch!
}

バックグラウンド取得の開始

メイン API は Service Worker の登録からハングするため、先に Service Worker を登録してください。以下の手順を行います。

navigator.serviceWorker.ready.then(async (swReg) => {
  const bgFetch = await swReg.backgroundFetch.fetch('my-fetch', ['/ep-5.mp3', 'ep-5-artwork.jpg'], {
    title: 'Episode 5: Interesting things.',
    icons: [{
      sizes: '300x300',
      src: '/ep-5-icon.png',
      type: 'image/png',
    }],
    downloadTotal: 60 * 1024 * 1024,
  });
});

backgroundFetch.fetch は、次の 3 つの引数を取ります。

パラメータ
id string
このバックグラウンド取得を一意に識別します。

ID が既存のバックグラウンド フェッチと一致する場合、backgroundFetch.fetch は拒否されます。

requests Array<Request|string>
取得対象。文字列は URL として扱われ、new Request(theString) によって Request に変換されます。

リソースが CORS を介して許可している限り、他のオリジンから物事を取得できます。

注: 現在、Chrome は CORS プリフライトを必要とするリクエストをサポートしていません。

options 以下を含むオブジェクト。
options.title string
進行状況とともに表示されるブラウザのタイトル。
options.icons Array<IconDefinition>
src、size、type を持つオブジェクトの配列。
options.downloadTotal number
レスポンス本文の合計サイズ(gzip 圧縮解除後)。

この情報は省略可能ですが、指定することを強くおすすめします。サイズと進行状況をユーザーに伝えます。指定しない場合、ブラウザはユーザーにサイズが不明であると通知するため、ユーザーがダウンロードを中止する可能性が高くなります。

バックグラウンドでの取得がここで指定した数を超えると、中止されます。ダウンロードのサイズが downloadTotal より小さくてもまったく問題ありません。そのため、ダウンロードの合計量が不明な場合は、慎重に判断してください。

backgroundFetch.fetch は、BackgroundFetchRegistration で解決される Promise を返します。これについては後で説明します。ユーザーがダウンロードを無効にしている場合、または指定されたパラメータのいずれかが無効な場合、Promise は拒否されます。

1 回のバックグラウンド取得に対して多数のリクエストを提供することで、ユーザーにとって論理的に 1 つのものを組み合わせることができます。たとえば、映画は数千ものリソース(MPEG-DASH で一般的)に分割され、画像などの追加リソースが含まれる場合があります。ゲームの 1 つのレベルが、多数の JavaScript、画像、音声リソースに分散される場合があります。しかしユーザーにとっては、それは単に「映画」または「レベル」です。

既存のバックグラウンド取得を取得する

既存のバックグラウンド取得は次のように取得できます。

navigator.serviceWorker.ready.then(async (swReg) => {
  const bgFetch = await swReg.backgroundFetch.get('my-fetch');
});

...必要なバックグラウンド フェッチの id を渡すことで、get は、その ID のアクティブなバックグラウンド フェッチがない場合、undefined を返します。

バックグラウンド フェッチは、登録されてから、成功、失敗、または中止されるまで「アクティブ」とみなされます。

getIds を使用すると、アクティブなバックグラウンド取得のリストを取得できます。

navigator.serviceWorker.ready.then(async (swReg) => {
  const ids = await swReg.backgroundFetch.getIds();
});

バックグラウンド フェッチの登録

BackgroundFetchRegistration(上記の例では bgFetch)は次のとおりです。

プロパティ
id string
バックグラウンド取得の ID。
uploadTotal number
サーバーに送信されるバイト数。
uploaded number
正常に送信されたバイト数。
downloadTotal number
バックグラウンド フェッチの登録時に提供された値、またはゼロ。
downloaded number
正常に受信したバイト数。

この値は減少する可能性があります。たとえば、接続が切断され、ダウンロードを再開できない場合、ブラウザはそのリソースの取得をゼロから再開します。

result

次のいずれかになります。

  • "" - バックグラウンド取得がアクティブなため、まだ結果がありません。
  • "success" - バックグラウンドの取得が成功しました。
  • "failure" - バックグラウンドでの取得に失敗しました。この値は、バックグラウンドでの取得が完全に失敗した場合にのみ表示されます(ブラウザでの再試行や再開ができないため)。
failureReason

次のいずれかになります。

  • "" - バックグラウンド取得が失敗していない。
  • "aborted" – バックグラウンド取得がユーザーによって中止されたか、abort() が呼び出された。
  • "bad-status" - レスポンスの 1 つのステータスが「OK」ではありません(例: 404)。
  • "fetch-error" - CORS、MIX、無効な部分レスポンス、再試行できないフェッチの一般的なネットワーク障害など、他の理由でフェッチに失敗しました。
  • "quota-exceeded" - バックグラウンドでの取得中に保存容量の上限に達しました。
  • "download-total-exceeded" - 指定された「downloadTotal」が超過しました。
recordsAvailable boolean
基盤となるリクエスト/レスポンスにアクセスできますか?

false に設定すると、matchmatchAll は使用できなくなります。

Methods
abort() Promise<boolean> を返します。
バックグラウンド取得を中止します。

フェッチが正常に中止された場合、返された Promise は true で解決されます。

matchAll(request, opts) 戻り値 Promise<Array<BackgroundFetchRecord>>
リクエストとレスポンスを取得します。

ここでの引数は、Cache API と同じです。引数なしで呼び出すと、すべてのレコードの Promise が返されます。

詳細は以下のとおりです。

match(request, opts) 上記と同様に Promise<BackgroundFetchRecord>
を返しますが、最初の一致で解決されます。
イベント
progress uploadeddownloadedresultfailureReason のいずれかが変更されたときに呼び出されます。

進捗状況の追跡

これを行うには、progress イベントを使用します。downloadTotal は指定した値になり、値を指定しない場合は 0 になります。

bgFetch.addEventListener('progress', () => {
  // If we didn't provide a total, we can't provide a %.
  if (!bgFetch.downloadTotal) return;

  const percent = Math.round(bgFetch.downloaded / bgFetch.downloadTotal * 100);
  console.log(`Download progress: ${percent}%`);
});

リクエストとレスポンスの取得

bgFetch.match('/ep-5.mp3').then(async (record) => {
  if (!record) {
    console.log('No record found');
    return;
  }

  console.log(`Here's the request`, record.request);
  const response = await record.responseReady;
  console.log(`And here's the response`, response);
});

recordBackgroundFetchRecord で、次のようになります。

プロパティ
request Request
提供されたリクエスト。
responseReady Promise<Response>
取得したレスポンス。

レスポンスがまだ受け取っていない可能性があるため、Promise の背後になっています。フェッチが失敗すると、Promise は拒否されます。

Service Worker イベント

イベント
backgroundfetchsuccess すべてが正常に取得されました。
backgroundfetchfailure 1 つ以上の取得が失敗しました。
backgroundfetchabort 1 つ以上の取得が失敗しました。

これは、関連データのクリーンアップを行う場合にのみ有用です。

backgroundfetchclick ユーザーがダウンロード進行状況 UI をクリックした。

イベント オブジェクトには次のものがあります。

プロパティ
registration BackgroundFetchRegistration
Methods
updateUI({ title, icons }) 初期設定しているタイトルまたはアイコンを変更できます。これは省略可能ですが、必要に応じて詳細なコンテキストを指定できます。これは、backgroundfetchsuccess イベントと backgroundfetchfailure イベント中に *1 回だけ* 実行できます。

成功/失敗への対応

progress イベントはすでに確認しましたが、これが役立つのは、ユーザーがサイトのページを開いている間だけです。バックグラウンド取得の主なメリットは、ユーザーがページから離れた後、またはブラウザを閉じた後も処理が継続されることです。

バックグラウンド取得が正常に完了すると、Service Worker は backgroundfetchsuccess イベントを受信し、event.registration はバックグラウンド取得の登録です。

このイベントの後、フェッチ済みのリクエストとレスポンスにはアクセスできなくなるため、保持する場合は、キャッシュ API などに移動します。

ほとんどの Service Worker イベントと同様に、event.waitUntil を使用して、イベントの完了を Service Worker が認識できるようにします。

たとえば、Service Worker では次のようになります。

addEventListener('backgroundfetchsuccess', (event) => {
  const bgFetch = event.registration;

  event.waitUntil(async function() {
    // Create/open a cache.
    const cache = await caches.open('downloads');
    // Get all the records.
    const records = await bgFetch.matchAll();
    // Copy each request/response across.
    const promises = records.map(async (record) => {
      const response = await record.responseReady;
      await cache.put(record.request, response);
    });

    // Wait for the copying to complete.
    await Promise.all(promises);

    // Update the progress notification.
    event.updateUI({ title: 'Episode 5 ready to listen!' });
  }());
});

失敗の原因は、1 つの 404 にすぎないかもしれないため、それほど重要ではなかったかもしれません。そのため、上記のように一部のレスポンスをキャッシュに保存することには意義があります。

クリックへの反応

ダウンロードの進行状況と結果を表示する UI がクリック可能になっている。Service Worker の backgroundfetchclick イベントを使用すると、これに対応できます。上記のように、event.registration はバックグラウンド取得の登録です。

このイベントでよく行うのは、ウィンドウを開くことです。

addEventListener('backgroundfetchclick', (event) => {
  const bgFetch = event.registration;

  if (bgFetch.result === 'success') {
    clients.openWindow('/latest-podcasts');
  } else {
    clients.openWindow('/download-progress');
  }
});

補足資料

修正: この記事の以前のバージョンでは、バックグラウンド取得が「ウェブ標準」と誤って記載されていました。この API は現在標準版に移行していません。この仕様は WICG のドラフト コミュニティ グループ レポートで確認できます。