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

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

概要

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

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

始める前に

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

拡張機能を作成する

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

ステップ 1: Service Worker を登録する

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

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 のデバッグ

Service Worker のログを見つける方法と、Service Worker の終了を確認する方法について説明します。まず、パッケージ化されていない拡張機能を読み込むの手順に沿って操作します。

30 秒後、「Service Worker (inactive)」と表示されます。これは、Service Worker が終了したことを意味します。[Service Worker (inactive)] リンクをクリックして調べます。次のアニメーションは、これを示しています。

Service Worker を調べると、Service Worker が起動されることに気づきましたか?DevTools で 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.

拡張機能 Service Worker をデバッグするその他の方法については、拡張機能のデバッグをご覧ください。

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

Chrome では、不要になると Service Worker はシャットダウンされます。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 を使用して値を保存することはできません。また、Service Worker は実行時間が短いため、ユーザーのブラウザ セッション中に繰り返し終了するため、グローバル変数との互換性がありません。代わりに、ローカルマシンにデータを保存する chrome.storage.local を使用してください。

拡張機能 Service Worker のその他のストレージ オプションについては、グローバル変数を使用せずにデータを保持するをご覧ください。

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

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

この例では 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() メソッドは、遅延タスクや定期的なタスクを実行するためによく使用されます。ただし、Service Worker の終了時にスケジューラがタイマーをキャンセルするため、これらの API は失敗する可能性があります。代わりに、拡張機能で chrome.alarms API を使用できます。

まず、マニフェストで "alarms" 権限をリクエストします。また、リモートでホストされる場所から拡張機能のヒントを取得するには、ホストの権限をリクエストする必要があります。

manifest.json:

{
  ...
  "permissions": ["storage", "alarms"],
  "permissions": ["storage"],
  "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 を開くクイック API 拡張機能。

今日のヒントを開く

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

今日のヒントを開く場所:
今日のヒントを始めるクイック API 拡張機能。

📊? 潜在的な機能強化

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

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

構築を続けましょう!

お疲れさまでした!このチュートリアルはこれで終了です🎉?。他の初心者向けチュートリアルを完了して、スキルアップを続けましょう。

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

引き続き探求を

拡張機能 Service Worker の学習プログラムを続けるには、次の記事を確認することをおすすめします。