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

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

概要

このチュートリアルでは、Chrome 拡張機能の Service Worker の概要について説明します。このチュートリアルでは、ユーザーがアドレスバーを使用して Chrome API リファレンス ページにすばやく移動できるようにする拡張機能を作成します。ここでは以下について学びます。

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

始める前に

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

拡張機能をビルドする

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

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

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

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"
  }
}

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

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

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

ステップ 2: 複数の Service Worker モジュールをインポートする

この Service Worker は 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");

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

省略可: Service Worker のデバッグ

サービス ワーカーのログを探し、終了したタイミングを確認する方法を説明します。まず、手順に沿って解凍した拡張機能を読み込みます。

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

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

次に、拡張機能を中断して、エラーの場所を確認します。たとえば、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 によってシャットダウンされます。Google では、chrome.storage API を使用して、Service Worker セッション間で状態を保持しています。ストレージにアクセスするには、マニフェストで権限をリクエストする必要があります。

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']
    });
  }
});

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

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

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

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

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

manifest.json:

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

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

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"],
  "host_permissions": ["https://extension-tips.glitch.me/*"],
}

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

sw-tips.js:

// Fetch tip & save in storage
const updateTip = async () => {
  const response = await fetch('https://extension-tips.glitch.me/tips.json');
  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 リファレンス ページにアクセスすると、拡張機能のコンテンツ スクリプトによってページが更新されます。メッセージを送信して、Service Worker から今日のヒントをリクエストします。

まず、マニフェストでコンテンツ スクリプトを宣言し、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;
}

最後のステップでは、メッセージ ハンドラを Service Worker に追加します。このメッセージ ハンドラは、コンテンツ スクリプトに毎日のヒントとともに返信します。

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 Extensions リファレンス API ページの追加ページを開きます。

構築を続けましょう。

このチュートリアルを完了しましたら、他の初心者向けチュートリアルを完了してスキルを磨いてください。

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

引き続き探求を

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