Service Worker に移行する

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

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

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

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

バックグラウンド スクリプトと拡張機能サービス ワーカーの違い

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

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

Service Worker は、バックグラウンド ページといくつかの点で異なります。

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

必要な変更

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

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

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

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

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

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

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

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

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

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

メッセージ パッシングを使用して、オフスクリーン ドキュメントと拡張機能サービス ワーカー間で通信します。

localStorage を別の型に変換する

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

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

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

拡張機能でのウェブ ストレージ API の動作には、いくつかのニュアンスもあります。詳しくは、ストレージと Cookie をご覧ください。

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

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

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

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

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

browser.action.onClicked.addListener(handleActionClick);

browser.storage.local.get(["badgeText"], ({ badgeText }) => {
  browser.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);

状態を永続化する

サービス ワーカーは一時的なもので、ユーザーのブラウザ セッション中に繰り返し開始、実行、終了する可能性があります。また、前のコンテキストが破棄されたため、データはグローバル変数ですぐに利用できません。この問題を回避するには、ストレージ API を信頼できる情報源として使用します。例でその方法を示します。

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

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

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

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

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

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

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

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

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

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

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

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

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

Service Worker を存続させる

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

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

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

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

このような場合に Service Worker の有効期間を延長するには、定期的に簡単な拡張機能 API を呼び出してタイムアウト カウンタをリセットします。これは例外的なケースでのみ使用されるもので、ほとんどの場合、同じ結果を得るためのより優れたプラットフォーム固有の方法があります。

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

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

waitUntil(someExpensiveCalculation());

Service Worker を継続的に存続させる

まれに、有効期間を無期限に延長する必要がある場合があります。企業と教育が最大のユースケースであると特定しており、特にこの 2 つのユースケースでは許可していますが、一般的にはサポートしていません。このような例外的な状況では、拡張機能の簡単な 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 browser.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 browser.storage.local.get('last-heartbeat'))['last-heartbeat'];
}