Service Worker でイベントを処理する

拡張機能 Service Worker のコンセプトを説明するチュートリアル

概要

このチュートリアルでは、Chrome 拡張機能のサービス ワーカーの概要について説明します。このチュートリアルでは、アドレスバーを使用して Chrome API リファレンス ページにすばやく移動できる拡張機能を構築します。ここでは以下について学びます。

  • Service Worker を登録してモジュールをインポートします。
  • 拡張機能のサービス ワーカーをデバッグします。
  • 状態を管理し、イベントを処理します。
  • 定期的なイベントをトリガーします。
  • コンテンツ スクリプトと通信します。

始める前に

このガイドは、基本的なウェブ開発の経験があることを前提としています。拡張機能の開発の概要については、Extensions 101Hello World をご覧になることをおすすめします。

拡張機能をビルドする

まず、拡張機能ファイルを格納する quick-api-reference という名前の新しいディレクトリを作成するか、GitHub サンプル リポジトリからソースコードをダウンロードします。

ステップ 1: サービス ワーカーを登録する

プロジェクトのルートにマニフェスト ファイルを作成し、次のコードを追加します。

manifest.json:

{
  "manifest_version": 3,
  "name": "Open extension API reference",
  "version": "1.0.0",
  "icons": {
    "16": "images/icon-16.png",
    "128": "images/icon-128.png"
  },
  "background": {
    "service_worker": "service-worker.js"
  }
}

拡張機能は、マニフェストでサービス ワーカーを登録します。マニフェストは 1 つの JavaScript ファイルのみを受け取ります。ウェブページのように navigator.serviceWorker.register() を呼び出す必要はありません。

images フォルダを作成し、そのフォルダにアイコンをダウンロードします。

マニフェスト内の拡張機能のメタデータアイコンについて詳しくは、読書時間チュートリアルの最初のステップをご覧ください。

ステップ 2: 複数のサービス ワーカー モジュールをインポートする

このサービス ワーカーは 2 つの機能を実装しています。保守性を高めるため、各機能を個別のモジュールに実装します。まず、マニフェストで Service Worker を ES モジュールとして宣言する必要があります。これにより、Service Worker でモジュールをインポートできるようになります。

manifest.json:

{
 "background": {
    "service_worker": "service-worker.js",
    "type": "module"
  },
}

service-worker.js ファイルを作成し、2 つのモジュールをインポートします。

import './sw-omnibox.js';
import './sw-tips.js';

これらのファイルを作成し、それぞれにコンソールログを追加します。

sw-omnibox.js:

console.log("sw-omnibox.js");

sw-tips.js:

console.log("sw-tips.js");

サービス ワーカーで複数のファイルをインポートするその他の方法については、スクリプトのインポートをご覧ください。

省略可: サービス ワーカーのデバッグ

サービス ワーカーのログを見つけて、終了したタイミングを確認する方法について説明します。まず、手順に沿ってパッケージ化されていない拡張機能を読み込みます

30 秒後に「service worker (inactive)」と表示されます。これは、サービス ワーカーが終了したことを意味します。[service worker (inactive)] リンクをクリックして検査します。次のアニメーションは、これを示しています。

サービス ワーカーを検査すると、サービス ワーカーが起動することに気づきましたか?デベロッパー ツールでサービス ワーカーを開くと、サービス ワーカーはアクティブな状態を維持します。サービス ワーカーが終了したときに拡張機能が正しく動作するように、DevTools を閉じてください。

次に、拡張機能を分割して、エラーの場所を確認します。これを行う方法の 1 つは、service-worker.js ファイルの './sw-omnibox.js' インポートから「.js」を削除することです。Chrome は Service Worker を登録できません。

chrome://extensions に戻り、拡張機能を更新します。次の 2 つのエラーが表示されます。

Service worker registration failed. Status code: 3.

An unknown error occurred when fetching the script.

拡張機能のサービス ワーカーをデバッグするその他の方法については、拡張機能のデバッグをご覧ください。

ステップ 4: 状態を初期化する

サービス ワーカーが不要になった場合、Chrome はサービス ワーカーをシャットダウンします。chrome.storage API を使用して、サービス ワーカー セッション間で状態を保持します。ストレージ アクセスの場合、マニフェストで権限をリクエストする必要があります。

manifest.json:

{
  ...
  "permissions": ["storage"],
}

まず、デフォルトの候補をストレージに保存します。runtime.onInstalled() イベントをリッスンすることで、拡張機能が最初にインストールされたときに状態を初期化できます。

sw-omnibox.js:

...
// Save default API suggestions
chrome.runtime.onInstalled.addListener(({ reason }) => {
  if (reason === 'install') {
    chrome.storage.local.set({
      apiSuggestions: ['tabs', 'storage', 'scripting']
    });
  }
});

サービス ワーカーは window オブジェクトに直接アクセスできないため、window.localStorage を使用して値を保存することはできません。また、サービス ワーカーは短命な実行環境であり、ユーザーのブラウザ セッション全体で繰り返し終了するため、グローバル変数と互換性がありません。代わりに、ローカルマシンにデータを保存する chrome.storage.local を使用してください。

拡張機能サービス ワーカーの他のストレージ オプションについては、グローバル変数を使用するのではなくデータを永続化するをご覧ください。

ステップ 5: イベントを登録する

すべてのイベント リスナーは、サービス ワーカーのグローバル スコープで静的に登録する必要があります。つまり、イベント リスナーを非同期関数内にネストしないでください。これにより、サービス ワーカーの再起動時にすべてのイベント ハンドラが復元されることが保証されます。

この例では chrome.omnibox API を使用しますが、まずマニフェストでオムニボックス キーワード トリガーを宣言する必要があります。

manifest.json:

{
  ...
  "minimum_chrome_version": "102",
  "omnibox": {
    "keyword": "api"
  },
}

次に、スクリプトの最上位レベルでオムニボックスのイベント リスナーを登録します。ユーザーがアドレスバーにオムニボックス キーワード(api)を入力し、Tab キーまたはスペースキーを押すと、Chrome はストレージ内のキーワードに基づいて候補のリストを表示します。これらの候補を生成するのは、現在のユーザー入力と suggestResult オブジェクトを受け取る onInputChanged() イベントです。

sw-omnibox.js:

...
const URL_CHROME_EXTENSIONS_DOC =
  'https://developer.chrome.com/docs/extensions/reference/';
const NUMBER_OF_PREVIOUS_SEARCHES = 4;

// Display the suggestions after user starts typing
chrome.omnibox.onInputChanged.addListener(async (input, suggest) => {
  await chrome.omnibox.setDefaultSuggestion({
    description: 'Enter a Chrome API or choose from past searches'
  });
  const { apiSuggestions } = await chrome.storage.local.get('apiSuggestions');
  const suggestions = apiSuggestions.map((api) => {
    return { content: api, description: `Open chrome.${api} API` };
  });
  suggest(suggestions);
});

ユーザーが候補を選択すると、onInputEntered() が対応する Chrome API リファレンス ページを開きます。

sw-omnibox.js:

...
// Open the reference page of the chosen API
chrome.omnibox.onInputEntered.addListener((input) => {
  chrome.tabs.create({ url: URL_CHROME_EXTENSIONS_DOC + input });
  // Save the latest keyword
  updateHistory(input);
});

updateHistory() 関数はオムニボックスの入力を取得し、storage.local に保存します。これにより、最近の検索語句を後でオムニボックスの候補として使用できます。

sw-omnibox.js:

...
async function updateHistory(input) {
  const { apiSuggestions } = await chrome.storage.local.get('apiSuggestions');
  apiSuggestions.unshift(input);
  apiSuggestions.splice(NUMBER_OF_PREVIOUS_SEARCHES);
  return chrome.storage.local.set({ apiSuggestions });
}

ステップ 6: 定期的な予定を設定する

setTimeout() メソッドまたは setInterval() メソッドは、遅延タスクや定期タスクの実行によく使用されます。ただし、サービス ワーカーが終了するとスケジューラがタイマーをキャンセルするため、これらの API は失敗する可能性があります。代わりに、拡張機能は chrome.alarms API を使用できます。

まず、マニフェストで "alarms" 権限をリクエストします。

manifest.json:

{
  ...
  "permissions": ["storage"],
  "permissions": ["storage", "alarms"],
}

拡張機能はすべてのヒントを取得し、その中から 1 つをランダムに選択してストレージに保存します。ヒントを更新するために 1 日 1 回トリガーされるアラームを作成します。Chrome を閉じると、アラームは保存されません。そのため、アラームが存在するかどうかを確認し、存在しない場合は作成する必要があります。

sw-tips.js:

// Fetch tip & save in storage
const updateTip = async () => {
  const response = await fetch('https://chrome.dev/f/extension_tips/');
  const tips = await response.json();
  const randomIndex = Math.floor(Math.random() * tips.length);
  return chrome.storage.local.set({ tip: tips[randomIndex] });
};

const ALARM_NAME = 'tip';

// Check if alarm exists to avoid resetting the timer.
// The alarm might be removed when the browser session restarts.
async function createAlarm() {
  const alarm = await chrome.alarms.get(ALARM_NAME);
  if (typeof alarm === 'undefined') {
    chrome.alarms.create(ALARM_NAME, {
      delayInMinutes: 1,
      periodInMinutes: 1440
    });
    updateTip();
  }
}

createAlarm();

// Update tip once a day
chrome.alarms.onAlarm.addListener(updateTip);

ステップ 7: 他のコンテキストと通信する

拡張機能は、コンテンツ スクリプトを使用して、ページのコンテンツを読み取り、変更します。ユーザーが Chrome API リファレンス ページにアクセスすると、拡張機能のコンテンツ スクリプトがその日のヒントでページを更新します。サービス ワーカーに今日のヒントをリクエストするメッセージを送信します。

まず、マニフェストでコンテンツ スクリプトを宣言し、Chrome API リファレンス ドキュメントに対応する一致パターンを追加します。

manifest.json:

{
  ...
  "content_scripts": [
    {
      "matches": ["https://developer.chrome.com/docs/extensions/reference/*"],
      "js": ["content.js"]
    }
  ]
}

新しいコンテンツ ファイルを作成します。次のコードは、チップをリクエストするメッセージをサービス ワーカーに送信します。次に、拡張機能のヒントを含むポップオーバーを開くボタンを追加します。このコードでは、新しいウェブ プラットフォームの Popover API を使用しています。

content.js:

(async () => {
  // Sends a message to the service worker and receives a tip in response
  const { tip } = await chrome.runtime.sendMessage({ greeting: 'tip' });

  const nav = document.querySelector('.upper-tabs > nav');
  
  const tipWidget = createDomElement(`
    <button type="button" popovertarget="tip-popover" popovertargetaction="show" style="padding: 0 12px; height: 36px;">
      <span style="display: block; font: var(--devsite-link-font,500 14px/20px var(--devsite-primary-font-family));">Tip</span>
    </button>
  `);

  const popover = createDomElement(
    `<div id='tip-popover' popover style="margin: auto;">${tip}</div>`
  );

  document.body.append(popover);
  nav.append(tipWidget);
})();

function createDomElement(html) {
  const dom = new DOMParser().parseFromString(html, 'text/html');
  return dom.body.firstElementChild;
}

最後の手順は、毎日のヒントを含む返信をコンテンツ スクリプトに送信するメッセージ ハンドラをサービス ワーカーに追加することです。

sw-tips.js:

...
// Send tip to content script via messaging
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.greeting === 'tip') {
    chrome.storage.local.get('tip').then(sendResponse);
    return true;
  }
});

動作をテストする

プロジェクトのファイル構造が次のようになっていることを確認します。

拡張機能フォルダの内容: images フォルダ、manifest.json、service-worker.js、sw-omnibox.js、sw-tips.js、content.js

拡張機能をローカルで読み込む

デベロッパー モードでパッケージ化されていない拡張機能を読み込むには、Hello world の手順に沿って操作します。

リファレンス ページを開く

  1. ブラウザのアドレスバーにキーワード「api」を入力します。
  2. 「Tab」キーまたは「Space」キーを押します。
  3. API の完全な名前を入力します。
    • または、過去の検索結果のリストから選択します。
  4. 新しいページが開き、Chrome API リファレンス ページが表示されます。

次のようになります。

ランタイム API リファレンスを開くクイック API リファレンス
Runtime API を開くクイック API 拡張機能。

今日のヒントを開く

ナビゲーション バーにあるヒントボタンをクリックして、拡張機能のヒントを開きます。

毎日のヒントを開く
今日のヒントを開くクイック API 拡張機能。

🎯 潜在的な改善点

今日の学習内容に基づいて、次のいずれかを試してください。

  • オムニボックスの候補を実装する別の方法を検討します。
  • 拡張機能のヒントを表示するための独自のカスタム モーダルを作成します。
  • MDN の Web 拡張機能リファレンス API ページへの追加のページを開きます。

構築を続けましょう。

このチュートリアルを完了しました 🎉。他の初心者向けチュートリアルを完了して、スキルをさらに向上させましょう。

広告表示オプション 学習内容
読書時間 特定のページセットに要素を自動的に挿入するには:
タブ マネージャー ブラウザタブを管理するポップアップを作成します。
フォーカス モード 拡張機能のアクションをクリックした後に現在のページでコードを実行します。

引き続き探求を

拡張機能のサービス ワーカーの学習パスを続けるには、次の記事を読むことをおすすめします。