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

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

概要

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

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

始める前に

このガイドは、基本的なウェブ開発の経験があることを前提としています。拡張機能の開発の概要については、拡張機能 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"
  }
}

拡張機能は、マニフェストに Service Worker を登録します。マニフェストは JavaScript ファイルを 1 つだけ受け取ります。 ウェブページのように 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");

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 によってシャットダウンされます。Service Worker セッション間で状態を保持するには、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']
    });
  }
});

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

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

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

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

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

manifest.json:

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

次に、アドレスバーのイベント リスナーをスクリプトのトップレベルに登録します。ユーザーがアドレスバーにアドレスバーのキーワード(api)を入力してから Tab キーまたはスペースを入力すると、ストレージ内のキーワードに基づいて候補のリストが表示されます。現在のユーザー入力と 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"],
  "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"]
    }
  ]
}

新しいコンテンツ ファイルを作成します。次のコードは、チップをリクエストするメッセージを Service Worker に送信します。次に、拡張機能のヒントを含むポップオーバーを開くボタンを追加します。このコードは、新しいウェブ プラットフォームの 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 を開く Quick API 拡張機能。

今日のヒントを開く

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

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

🙁? 潜在的な強化

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

  • アドレスバーの提案機能を実装する別の方法を確認する。
  • 拡張機能のヒントを表示するための独自のカスタム モーダルを作成します。
  • MDN の Web Extensions リファレンス API ページへの追加ページを開きます。

構築を続けましょう。

以上でこのチュートリアルは終了です 🎉?。他のコースを修了してスキルをレベルアップさせましょう 初心者向けチュートリアル:

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

引き続き探求を

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