Service Worker に移行する

バックグラウンド ページまたはイベント ページを Service Worker で置き換える

Service Worker は、拡張機能のバックグラウンド ページまたはイベントページを置き換えて、バックグラウンド コードがメインスレッドの外に出ないようにします。これにより、必要なときにだけ拡張機能を実行でき、リソースを節約できます。

バックグラウンド ページは、拡張機能が登場して以来、基本的なコンポーネントでした。簡単に言うと、バックグラウンド ページは、他のウィンドウやタブとは無関係に機能する環境を提供します。これにより、拡張機能はイベントを監視し、イベントに対応できます。

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

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

場合によっては、「バックグラウンド スクリプト」と呼ばれる拡張機能 Service Worker もあります。拡張機能 Service Worker はバックグラウンドで実行されますが、バックグラウンド スクリプトを呼び出すと、同じ機能を示すため、誤解を招きます。相違点については、以下で説明します。

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

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

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

必要な変更

バックグラウンド スクリプトと Service Worker の動作の違いを考慮して、いくつかのコードを調整する必要があります。まず、マニフェスト ファイルでの Service Worker の指定方法は、バックグラウンド スクリプトの指定方法とは異なります。さらに、次のようなことが可能です。

  • 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" フィールドには 1 つの文字列を指定します。"type" フィールドは、ES モジュールを使用する場合(import キーワードを使用)にのみ必要です。値は常に "module" です。詳細については、拡張機能 Service Worker の基本をご覧ください。

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

一部の拡張機能では、新しいウィンドウやタブを視覚的に開かずに DOM や window オブジェクトにアクセスする必要があります。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 からアクセス可能)は、Service Worker で使用できません。この問題を解決するには、次の 2 つのいずれかの操作を行います。まず、別のストレージ メカニズムの呼び出しに置き換えることができます。chrome.storage.local 名前空間はほとんどのユースケースで使用できますが、他のオプションを使用することもできます。

その通話をオフスクリーン ドキュメントに移動することもできます。たとえば、以前 localStorage に保存されていたデータを別のメカニズムに移行するには、次のようにします。

  1. 変換ルーチンと runtime.onMessage ハンドラを使用して、画面外ドキュメントを作成します。
  2. 画面外ドキュメントに変換ルーチンを追加します。
  3. 拡張機能 Service Worker で chrome.storage のデータを確認します。
  4. データが見つからない場合は、画面外ドキュメントを作成し、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 });
});

Manifest 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() メソッドで、遅延オペレーションや周期オペレーションを使用するのが一般的です。ただし、Service Worker が終了するとタイマーがキャンセルされるため、Service Worker でこれらの 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 の存続期間を長くするために、追加の対策が必要になることがあります。

長時間実行オペレーションが終了するまで Service Worker を維持する

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

  • fetch() リクエストが 5 分以上かかる可能性がある(接続状況が良くない可能性がある大量のダウンロードなど)。
  • 30 秒以上かかる複雑な非同期計算。

このような場合に Service Worker の有効期間を延長するには、簡単な拡張 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 拡張機能チームは、今後これらの拡張機能に対して措置を講じる権限を有します。

Service Worker が動作し続けるようにするには、次のコード スニペットを使用します。

/**
 * 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'];
}