Service Worker に移行する

背景ページまたはイベントページをサービス ワーカーに置き換える

サービス ワーカーは、拡張機能のバックグラウンド ページまたはイベントページに代わるものであり、バックグラウンド コードがメインスレッドに影響しないようにします。これにより、拡張機能が必要なときだけ実行されるため、リソースを節約できます。

バックグラウンド ページは、導入以来、拡張機能の基本的なコンポーネントとなっています。簡単に言えば、バックグラウンド ページは、他のウィンドウやタブとは独立した環境を提供します。これにより、拡張機能はイベントを監視し、イベントに応じてアクションを実行できます。

このページでは、バックグラウンド ページを拡張機能 Service Worker に変換するタスクについて説明します。拡張機能サービス ワーカーの一般的な詳細については、チュートリアルのサービス ワーカーでイベントを処理する拡張機能サービス ワーカーについてをご覧ください。

バックグラウンド スクリプトと拡張機能 Service Worker の違い

コンテキストによっては、拡張機能の Service Worker が「バックグラウンド スクリプト」と呼ばれることがあります。拡張機能の Service Worker はバックグラウンドで実行されますが、バックグラウンド スクリプトと呼ぶと、同じ機能を暗示することになり、誤解を招く可能性があります。相違点については、以下で説明します。

バックグラウンド ページからの変更

Service Worker は、バックグラウンド ページといくつかの違いがあります。

  • メインスレッドから機能するため、拡張機能のコンテンツを妨げません。
  • 拡張機能のオリジンでフェッチ イベントをインターセプトする(ツールバーのポップアップからのフェッチ イベントなど)などの特別な機能があります。
  • クライアント インターフェースを介して、他のコンテキストと通信したり、やり取りしたりできます。

必要な変更

バックグラウンド スクリプトとサービス ワーカーの動作の違いを考慮して、コードをいくつか調整する必要があります。まず、マニフェスト ファイルでサービス ワーカーを指定する方法は、バックグラウンド スクリプトを指定する方法とは異なります。また、次の方法もあります。

  • これらの API は DOM または window インターフェースにアクセスできないため、このような呼び出しを別の API またはオフスクリーン ドキュメントに移動する必要があります。
  • イベント リスナーは、返された Promise に応答して登録したり、イベント コールバック内で登録したりしないでください。
  • XMLHttpRequest() との下位互換性がないため、このインターフェースの呼び出しを fetch() の呼び出しに置き換える必要があります。
  • 使用されていないときは終了するため、グローバル変数に依存するのではなく、アプリケーションの状態を保持する必要があります。Service Worker を終了すると、タイマーが完了する前に終了することもあります。アラームに置き換える必要があります。

このページでは、これらのタスクについて詳しく説明します。

マニフェストの「background」フィールドを更新する

Manifest V3 では、バックグラウンド ページが Service Worker に置き換えられます。マニフェストの変更は次のとおりです。

  • manifest.json"background.scripts""background.service_worker" に置き換えます。"service_worker" フィールドは文字列を受け取ります。文字列の配列は受け取りません。
  • manifest.json から "background.persistent" を削除します。
Manifest V2
{
  ...
  "background": {
    "scripts": [
      "backgroundContextMenus.js",
      "backgroundOauth.js"
    ],
    "persistent": false
  },
  ...
}
Manifest V3
{
  ...
  "background": {
    "service_worker": "service_worker.js",
    "type": "module"
  }
  ...
}

"service_worker" フィールドには単一の文字列を指定します。"type" フィールドは、ES モジュールimport キーワードを使用)を使用する場合にのみ必要です。この値は常に "module" です。詳しくは、拡張機能 Service Worker の基本をご覧ください。

DOM 呼び出しとウィンドウ呼び出しをオフスクリーン ドキュメントに移動

一部の拡張機能では、新しいウィンドウやタブを視覚的に開かずに DOM オブジェクトとウィンドウ オブジェクトにアクセスする必要があります。Offscreen API を使用すると、拡張機能でパッケージ化された非表示のドキュメントを開いたり閉じたりできるため、ユーザー エクスペリエンスを損なうことなくこれらのユースケースに対応できます。メッセージ パススルーを除き、オフスクリーン ドキュメントは他の拡張機能コンテキストと API を共有せず、拡張機能が操作する完全なウェブページとして機能します。

Offscreen API を使用するには、Service Worker から画面外ドキュメントを作成します。

chrome.offscreen.createDocument({
  url: chrome.runtime.getURL('offscreen.html'),
  reasons: ['CLIPBOARD'],
  justification: 'testing the offscreen API',
});

画面外ドキュメントで、以前はバックグラウンド スクリプトで実行していたアクションを実行します。たとえば、ホストページで選択したテキストをコピーできます。

let textEl = document.querySelector('#text');
textEl.value = data;
textEl.select();
document.execCommand('copy');

メッセージの受け渡しを使用して、画面外のドキュメントと拡張機能 Service Worker の間でやり取りします。

localStorage を別の型に変換する

ウェブ プラットフォームの Storage インターフェース(window.localStorage からアクセス可能)は、サービス ワーカーでは使用できません。この問題に対処するには、次のいずれかを行います。まず、別のストレージ メカニズムの呼び出しに置き換えることができます。ほとんどのユースケースでは chrome.storage.local 名前空間を使用できますが、他のオプションも使用できます。

呼び出しを画面外のドキュメントに移動することもできます。たとえば、以前に localStorage に保存されていたデータを別のメカニズムに移行するには、次のようにします。

  1. コンバージョン ルーティンと runtime.onMessage ハンドラを使用して、画面外ドキュメントを作成します。
  2. 画面外ドキュメントにコンバージョン ルーティンを追加します。
  3. 拡張機能サービス ワーカーで、データの chrome.storage を確認します。
  4. データが見つからない場合は、オフスクリーン ドキュメントをcreateし、runtime.sendMessage() を呼び出して変換ルーティンを開始します。
  5. 画面外ドキュメントに追加した runtime.onMessage ハンドラで、変換ルーティンを呼び出します。

また、拡張機能での Web Storage API の動作には、いくつかの違いがあります。詳しくは、ストレージと Cookie をご覧ください。

リスナーを同期的に登録する

リスナーを非同期(Promise 内やコールバック内など)に登録しても、Manifest V3 での動作は保証されません。次のコードについて考えてみましょう。

chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
  chrome.browserAction.setBadgeText({ text: badgeText });
  chrome.browserAction.onClicked.addListener(handleActionClick);
});

これは、ページが常に実行され、再初期化されないため、永続的なバックグラウンド ページで機能します。Manifest V3 では、イベントがディスパッチされたときに Service Worker が再初期化されます。つまり、イベントが発生したときに、リスナーは登録されません(非同期で追加されるため)し、イベントは発生しません。

代わりに、イベント リスナーの登録をスクリプトの最上位に移動します。これにより、拡張機能の起動ロジックの実行が完了していなくても、Chrome はアクションのクリック ハンドラをすぐに検出して呼び出せるようになります。

chrome.action.onClicked.addListener(handleActionClick);

chrome.storage.local.get(["badgeText"], ({ badgeText }) => {
  chrome.action.setBadgeText({ text: badgeText });
});

XMLHttpRequest() をグローバル fetch() に置き換える

XMLHttpRequest() は、Service Worker、拡張機能などから呼び出すことはできません。バックグラウンド スクリプトからの XMLHttpRequest() への呼び出しを、グローバル fetch() の呼び出しに置き換えます。

XMLHttpRequest()
const xhr = new XMLHttpRequest();
console.log('UNSENT', xhr.readyState);

xhr.open('GET', '/api', true);
console.log('OPENED', xhr.readyState);

xhr.onload = () => {
    console.log('DONE', xhr.readyState);
};
xhr.send(null);
fetch()
const response = await fetch('https://www.example.com/greeting.json'')
console.log(response.statusText);

状態を保持する

Service Worker はエフェメラルです。つまり、ユーザーのブラウザ セッション中に起動、実行、終了が繰り返し行われます。また、以前のコンテキストが破棄されたため、グローバル変数でデータをすぐに使用できないことも意味します。この問題を回避するには、信頼できる情報源としてストレージ API を使用します。例を交えて説明します。

次の例では、グローバル変数を使用して名前を格納します。Service Worker では、この変数はユーザーのブラウザ セッション中に複数回リセットされる可能性があります。

Manifest V2 のバックグラウンド スクリプト
let savedName = undefined;

chrome.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    savedName = name;
  }
});

chrome.browserAction.onClicked.addListener((tab) => {
  chrome.tabs.sendMessage(tab.id, { name: savedName });
});

マニフェスト V3 の場合は、グローバル変数を Storage API の呼び出しに置き換えます。

Manifest V3 Service Worker
chrome.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    chrome.storage.local.set({ name });
  }
});

chrome.action.onClicked.addListener(async (tab) => {
  const { name } = await chrome.storage.local.get(["name"]);
  chrome.tabs.sendMessage(tab.id, { name });
});

タイマーをアラームに変更する

setTimeout() メソッドまたは setInterval() メソッドを使用して、遅延オペレーションまたは定期オペレーションを使用するのが一般的です。ただし、サービス ワーカーでは、サービス ワーカーが終了するたびにタイマーがキャンセルされるため、これらの API が失敗することがあります。

Manifest V2 のバックグラウンド スクリプト
// 3 minutes in milliseconds
const TIMEOUT = 3 * 60 * 1000;
setTimeout(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
}, TIMEOUT);

代わりに Alarms API を使用してください。他のリスナーと同様に、アラーム リスナーはスクリプトの最上位に登録する必要があります。

Manifest V3 Service Worker
async function startAlarm(name, duration) {
  await chrome.alarms.create(name, { delayInMinutes: 3 });
}

chrome.alarms.onAlarm.addListener(() => {
  chrome.action.setIcon({
    path: getRandomIconPath(),
  });
});

Service Worker を存続させる

定義上、Service Worker はイベント ドリブンであり、非アクティブになると終了します。これにより、Chrome は拡張機能のパフォーマンスとメモリ消費を最適化できます。詳しくは、サービス ワーカーのライフサイクルに関するドキュメントをご覧ください。例外として、Service Worker を長時間存続させるために、追加の対策が必要になる場合があります。

長時間実行オペレーションが完了するまで Service Worker を存続させる

拡張機能 API を呼び出さない長時間実行の Service Worker オペレーション中に、Service Worker がオペレーションの途中でシャットダウンすることがあります。次に例を示します。

  • 5 分を超える時間がかかる可能性のある fetch() リクエスト(接続が不安定な状態での大量のダウンロードなど)。
  • 30 秒を超える複雑な非同期計算。

このような場合にサービス ワーカーの存続期間を延長するには、単純な拡張 API を定期的に呼び出してタイムアウト カウンタをリセットします。これは例外的なケースにのみ使用してください。ほとんどの状況では、同じ結果を達成するための、プラットフォーム固有のより良い方法があります。

次の例は、特定の Promise が解決されるまで Service Worker を存続させる waitUntil() ヘルパー関数を示しています。

async function waitUntil(promise) = {
  const keepAlive = setInterval(chrome.runtime.getPlatformInfo, 25 * 1000);
  try {
    await promise;
  } finally {
    clearInterval(keepAlive);
  }
}

waitUntil(someExpensiveCalculation());

Service Worker の継続的な稼働を維持する

まれに、有効期間を無期限に延長する必要がある場合があります。Google は、企業と教育機関が最大のユースケースであると認識しており、特にこのユースケースを許可していますが、基本的にはサポートしていません。このような例外的な状況では、単純な拡張機能 API を定期的に呼び出すことで、Service Worker を存続させることができます。この推奨事項は、企業や教育機関のユースケースで管理対象デバイスで実行される拡張機能にのみ適用されることに注意してください。それ以外のケースでは許可されません。Chrome 拡張機能チームは、今後、そのような拡張機能に対して措置を講じる権利を留保します。

次のコード スニペットを使用して、サービス ワーカーを存続させます。

/**
 * Tracks when a service worker was last alive and extends the service worker
 * lifetime by writing the current time to extension storage every 20 seconds.
 * You should still prepare for unexpected termination - for example, if the
 * extension process crashes or your extension is manually stopped at
 * chrome://serviceworker-internals. 
 */
let heartbeatInterval;

async function runHeartbeat() {
  await chrome.storage.local.set({ 'last-heartbeat': new Date().getTime() });
}

/**
 * Starts the heartbeat interval which keeps the service worker alive. Call
 * this sparingly when you are doing work which requires persistence, and call
 * stopHeartbeat once that work is complete.
 */
async function startHeartbeat() {
  // Run the heartbeat once at service worker startup.
  runHeartbeat().then(() => {
    // Then again every 20 seconds.
    heartbeatInterval = setInterval(runHeartbeat, 20 * 1000);
  });
}

async function stopHeartbeat() {
  clearInterval(heartbeatInterval);
}

/**
 * Returns the last heartbeat stored in extension storage, or undefined if
 * the heartbeat has never run before.
 */
async function getLastHeartbeat() {
  return (await chrome.storage.local.get('last-heartbeat'))['last-heartbeat'];
}