QR Send

スマホとPCのChrome間で、URLやテキストをQRコード1回のペアリングで送り合える自作Chrome拡張。アカウント不要・無料。

使ってみる →

Chrome拡張 Manifest V3のservice workerはすぐ寝る:chrome.alarmsで定期処理を成立させる

個人開発

Manifest V3でbackgroundの前提が変わった

Chrome拡張を作っていて最初につまずきやすいのが、Manifest V3(MV3)でのbackground処理の扱いです。Manifest V2まではbackground.htmlのような「常時起動しているページ」を1枚裏で持たせておくことができ、そこでsetIntervalを回しておけば延々とポーリングし続けるようなことが簡単にできました。

ところがMV3ではこの永続ページが廃止され、background処理は「service worker」というイベント駆動の仕組みに置き換わっています。service workerはイベントが来たときだけ起動し、何もイベントがなければ数十秒ほどで自動的に終了(terminate)してしまいます。常に生きている前提のコードを書くと、ここで静かに壊れます。

setIntervalが効かなくなる

一番よくあるハマりどころが、background.js内でsetIntervalを使って定期処理を組んでしまうケースです。

// これはMV3では動作が保証されない
setInterval(() => {
  checkForNewMessages();
}, 30000);

service workerが起動している間はタイマーも動きますが、数十秒操作がないとservice worker自体が終了し、それと同時にsetIntervalで仕込んだタイマーも消えてしまいます。次に何らかのイベントでservice workerが再起動したとき、setIntervalはもう一度最初から呼び直さない限り復活しません。実装した本人は「一応動いているように見える」のでテスト中は気づきにくく、リリース後にしばらく放置していたら通知が全然来ない、という形で発覚しがちなバグです。

chrome.alarmsで最短30秒周期の起床にする

MV3で定期処理をやりたい場合、正攻法はchrome.alarms APIです。これはOS側のアラーム機構を使ってservice workerを指定した間隔で確実に起こしてくれる仕組みで、service workerが休止していても指定時刻になればChromeがきちんと叩き起こしてくれます。

注意点として、chrome.alarmsの最短周期は30秒です。それより短い間隔を指定しても30秒に丸められます。数秒おきのリアルタイム処理には向いていませんが、「一定間隔でサーバーを確認する」程度のポーリング用途には十分です。

まずmanifest.jsonでalarms権限とservice workerを宣言します。

{
  "manifest_version": 3,
  "background": {
    "service_worker": "background.js"
  },
  "permissions": ["alarms", "storage"]
}

background.js側では、拡張のインストール時にアラームを登録しておきます。

chrome.runtime.onInstalled.addListener(() => {
  chrome.alarms.create("poll-check", { periodInMinutes: 0.5 });
});

periodInMinutes: 0.5が30秒周期にあたります。これでChromeが30秒ごとにservice workerを起こし、chrome.alarms.onAlarmイベントを発火してくれるようになります。

service workerは状態を保持できない

ここで見落としがちなのが、service workerはグローバル変数に状態を持たせておいても、休止のたびに消えてしまう点です。前回のチェックでどこまで処理したか、というカーソルやタイムスタンプをただの変数に入れていると、次に起こされたときには初期化された状態からスタートしてしまいます。

これを避けるには、保持したい状態は毎回chrome.storage.localに書き込んでおき、起こされるたびにそこから読み直す、という設計にする必要があります。service worker自身は「状態を持たない実行環境」だと割り切り、状態の置き場所は必ずstorageに寄せるということです。

実例:一定間隔でサーバーの新着を確認する

自作のChrome拡張「QR Send」(https://qrsend.app)でも、この構成をそのまま使っています。スマホから送られてきたURLを中継サーバー経由で受け取る仕組みで、PC側の拡張機能がservice workerとして30秒おきに起こされ、新着がないかを確認し、あれば新しいタブとして開く、という流れです。

chrome.alarms.onAlarm.addListener(async (alarm) => {
  if (alarm.name !== "poll-check") return;

  const { lastCheckedAt } = await chrome.storage.local.get("lastCheckedAt");
  const since = lastCheckedAt || 0;

  const res = await fetch(`https://example.com/messages?since=${since}`);
  const messages = await res.json();

  for (const message of messages) {
    chrome.tabs.create({ url: message.url });
  }

  await chrome.storage.local.set({ lastCheckedAt: Date.now() });
});

ポイントは、アラームが発火するたびにchrome.storage.localから前回チェック時刻を読み直し、処理が終わったら次回のために書き戻しているところです。service worker自体がその間に何度休止と再起動を繰り返していても、状態はstorageの中にあるので影響を受けません。

通知を出したい場合はchrome.notificationsを、新着があったことをバッジで示したい場合はchrome.action.setBadgeTextを、同じonAlarmリスナーの中に足していくだけで済みます。

まとめ

  • MV3のbackgroundは常時起動の永続ページではなく、イベント駆動のservice workerに変わり、数十秒で自動終了する
  • setIntervalはservice workerの休止とともに消えるため、定期処理には使えない
  • chrome.alarmsを使えば、休止していても指定間隔でChromeがservice workerを起こしてくれる。ただし最短周期は30秒
  • service workerはグローバル変数で状態を保持できないため、必要な状態はchrome.storage.localに永続化し、起こされるたびに読み直す
  • 「一定間隔でサーバーの新着を確認して通知やタブを開く」というポーリング処理は、このchrome.alarms+chrome.storageの組み合わせで十分に実現できる

コメント

タイトルとURLをコピーしました