Service Worker に移行する

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

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

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

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

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

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

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

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

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

必要な変更

バックグラウンド スクリプトと 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 オブジェクトとウィンドウ オブジェクトにアクセスする必要があります。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 では使用できません。これに対処するには、次のいずれかを行います。まず、別のストレージ メカニズムの呼び出しに置き換えることができます。chrome.storage.local 名前空間はほとんどのユースケースに対応しますが、他のオプションも利用できます。

呼び出しをオフスクリーン ドキュメントに移動することもできます。たとえば、以前に localStorage に保存したデータを別のメカニズムに移行するには、次の操作を行います。

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

拡張機能でのウェブ ストレージ 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 が終了するたびにタイマーがキャンセルされるため、これらの API は Service Worker で失敗する可能性があります。

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 秒以上かかる。

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

次の例は、指定された 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 を継続的に有効にする

まれに、有効期間を無期限に延長する必要がある場合があります。企業と教育機関が最大のユースケースであると特定し、そこでこれを許可していますが、一般的にはサポートしていません。このような例外的な状況では、トリビアルな拡張機能 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'];
}