バックグラウンド ページまたはイベントページを 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()の呼び出しに置き換える必要があります。- 使用されていないときに終了するため、グローバル変数に依存するのではなく、アプリケーションの状態を保持する必要があります。サービス ワーカーを終了すると、タイマーが完了する前に終了することもあります。アラームに置き換える必要があります。
このページでは、これらのタスクについて詳しく説明します。
マニフェストの「background」フィールドを更新する
Manifest V3 では、バックグラウンド ページが Service Worker に置き換えられます。マニフェストの変更点は以下のとおりです。
manifest.jsonの"background.scripts"を"background.service_worker"に置き換えます。"service_worker"フィールドは、文字列の配列ではなく文字列を受け取ります。manifest.jsonから"background.persistent"を削除します。
{ ... "background": { "scripts": [ "backgroundContextMenus.js", "backgroundOauth.js" ], "persistent": false }, ... }
{ ... "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 からオフスクリーン ドキュメントを作成します。
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');
メッセージ パッシングを使用して、オフスクリーン ドキュメントと拡張機能の Service Worker 間の通信を行います。
localStorage を別のタイプに変換する
ウェブ プラットフォームの Storage インターフェース(window.localStorage からアクセス可能)は、Service Worker では使用できません。この問題に対処するには、次の 2 つの方法のいずれかを行います。まず、別のストレージ メカニズムの呼び出しに置き換えることができます。browser.storage.local 名前空間はほとんどのユースケースに対応しますが、他のオプションも利用できます。
呼び出しをオフスクリーン ドキュメントに移動することもできます。たとえば、以前に localStorage に保存されたデータを別のメカニズムに移行するには、次のようにします。
- 変換ルーティンと
runtime.onMessageハンドラを使用して、画面外ドキュメントを作成します。 - 画面外ドキュメントに変換ルーティンを追加します。
- 拡張機能の Service Worker で、データの
browser.storageを確認します。 - データが見つからない場合は、オフスクリーン ドキュメントを作成し、
runtime.sendMessage()を呼び出して変換ルーティンを開始します。 - 画面外ドキュメントに追加した
runtime.onMessageハンドラで、変換ルーティンを呼び出します。
また、拡張機能でのウェブ ストレージ API の動作には、いくつかのニュアンスがあります。詳しくは、ストレージと Cookie をご覧ください。
リスナーを同期的に登録する
Manifest V3 では、リスナーを非同期で登録する(Promise やコールバック内など)ことは保証されていません。次のコードについて考えてみましょう。
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() は、サービス ワーカー、拡張機能などから呼び出すことはできません。バックグラウンド スクリプトから XMLHttpRequest() への呼び出しを、グローバル fetch() への呼び出しに置き換えます。
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);
const response = await fetch('https://www.example.com/greeting.json'') console.log(response.statusText);
状態を保持する
サービス ワーカーは一時的なものであるため、ユーザーのブラウザ セッション中に繰り返し開始、実行、終了される可能性があります。また、以前のコンテキストが破棄されているため、グローバル変数内のデータはすぐには利用できません。これを回避するには、信頼できる情報源として Storage API を使用します。例を使って、その方法を説明します。
次の例では、グローバル変数を使用して名前を保存しています。サービス ワーカーでは、ユーザーのブラウザ セッション中にこの変数が複数回リセットされる可能性があります。
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 }); });
Manifest V3 の場合は、グローバル変数を Storage API の呼び出しに置き換えます。
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() メソッドを使用して、遅延オペレーションまたは定期オペレーションを使用するのが一般的です。ただし、これらの API は、サービス ワーカーが終了するたびにタイマーがキャンセルされるため、サービス ワーカーで失敗する可能性があります。
// 3 minutes in milliseconds const TIMEOUT = 3 * 60 * 1000; setTimeout(() => { browser.action.setIcon({ path: getRandomIconPath(), }); }, TIMEOUT);
代わりに Alarms API を使用します。他のリスナーと同様に、アラーム リスナーはスクリプトの最上位に登録する必要があります。
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 を維持する
拡張機能 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());
サービス ワーカーを継続的に維持する
まれに、有効期間を無期限に延長する必要がある場合があります。企業と教育機関が最大のユースケースであると特定し、これらのユースケースでは特に許可していますが、一般的にはサポートしていません。このような例外的な状況では、簡単な拡張機能 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'];
}