現在の絞り込み: 技術ブログすべてクリア

Chrome拡張でDOMが操作できない? popupとscripting APIの関係を整理する

はじめに

chrome拡張には大別して2つのパターンがあります。
拡張アイコンをクリックした時に「ポップアップウィンドウが開くもの」と「ポップアップウィンドウは開かないもの」の2つです。
これは単にuiの違いだけではなく、scripting APIとactiveTabを使ったDOM操作のフローが根本的に変わるという特徴があります。
ここを理解せずにコードを書くと「コードは正しいはずなのに、なんでDOMが操作できないんだろう?」といったことになってしまうので、そうならないようにポイントをまとめました。


前提知識

  • activeTab — ユーザーが拡張アイコンをクリックした時にだけ、現在のタブへの一時的なアクセス権を付与する権限
  • scripting APIchrome.scripting.executeScript() でページにスクリプトを一時的に注入する仕組み
  • Service Worker(background.js) — ブラウザのバックグラウンドで動くプロセス。ページのDOMには直接アクセスできない

詳細は次の記事を参照:
Chrome拡張のスクリプト注入と権限設計 — content_scriptsのリスクとscripting APIによる解決策


最初に結論

Chrome拡張のアイコンクリック時の動作は、manifest.jsonでdefault_popupを指定するかどうかで二者択一になる。

  popupあり popupなし クリック時の動作 popup.htmlが開く action.onClickedイベントが発火する DOM操作の起点 popup.js background.js 用途 フォーム表示、情報の確認・編集 ワンクリックで即座に処理実行

この2つは排他的で、popupが設定されているとonClickedイベントは発火しない。

action.onClicked — Fired when an action icon is clicked. This event will not fire if the action has a popup.

参考: chrome.action API — Chrome for Developers


パターン1:popupなし — background.jsで処理する

どういう拡張か

アイコンをクリックしたら、ユーザーとのやりとりなしに即座に処理が走る拡張。例えば「ワンクリックでダークモードに切り替える」「今のページのスクリーンショットを撮る」「ページの背景色を変える」など。

manifest.json

default_popupを指定しない。

{
  "manifest_version": 3,
  "name": "One Click Extension",
  "version": "1.0",
  "action": {
    "default_title": "クリックで実行"
  },
  "permissions": [
    "activeTab",
    "scripting"
  ],
  "background": {
    "service_worker": "background.js"
  }
}

処理の流れ

ユーザーがアイコンをクリック
  ↓
action.onClicked イベントが発火(background.js)
  ↓
activeTab の権限が有効になる
  ↓
background.js から executeScript() でページにスクリプトを注入
  ↓
ページ内で関数が実行され、結果が background.js に返る

コード例

// background.js
chrome.action.onClicked.addListener(async (tab) => {
  const results = await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => {
      // ページのコンテキストで実行される
      document.body.style.backgroundColor = 'red';
      return { title: document.title };
    }
  });
  console.log(results[0].result);
});

Chrome公式のactiveTabチュートリアルもこのパターンで書かれている。

参考: Inject scripts into the active tab — Chrome for Developers


パターン2:popupあり — popup.jsで処理する

どういう拡張か

アイコンをクリックした後にフォームや情報を表示し、ユーザーが確認・操作してから処理を行う拡張。例えば「ブックマークツール」「翻訳ツール」「パスワードマネージャー」など。

manifest.json

default_popupを指定する。

{
  "manifest_version": 3,
  "name": "Popup Extension",
  "version": "1.0",
  "action": {
    "default_title": "ブックマークする",
    "default_popup": "popup.html"
  },
  "permissions": [
    "activeTab",
    "scripting"
  ],
  "background": {
    "service_worker": "background.js"
  }
}

処理の流れ

ユーザーがアイコンをクリック
  ↓
popup.html が開く(action.onClicked は発火しない)
  ↓
activeTab の権限が有効になる
  ↓
popup.js から executeScript() でページにスクリプトを注入
  ↓
ページ内で関数が実行され、結果が popup.js に返る
  ↓
popup.js が取得した値を popup.html のDOMに表示

コード例

<!-- popup.html -->
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
  <h3 id="title"></h3>
  <p id="url"></p>
  <p id="published_at"></p>
  <button id="save">保存</button>
  <script src="popup.js"></script>
</body>
</html>
// popup.js
document.addEventListener('DOMContentLoaded', async () => {
  // 現在のアクティブタブを取得
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });

  // ページにスクリプトを注入してメタデータを取得
  const results = await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => {
      return {
        title: document.title,
        url: window.location.href,
        published_at: document.querySelector('meta[property="article:published_time"]')
          ?.getAttribute('content')
          ?.slice(0, 10) || ""
      };
    }
  });

  const metadata = results[0].result;

  // popup.html のDOMに表示
  // popup.jsはpopup.htmlと同じレンダラープロセスで動くので、`document.getElementById()` で直接DOMを操作できる。
  document.getElementById('title').textContent = metadata.title;
  document.getElementById('url').textContent = metadata.url;
  document.getElementById('published_at').textContent = metadata.published_at;
});

混乱しやすいポイント

background.jsのonClickedでメタデータを取得してpopupに表示したら?

default_popup を指定すると onClicked イベント自体が発火しないので不可。

popup.jsからexecuteScript()は呼べないのでは?

popup.htmlが開いた時点で activeTab の権限が有効になるので、popup.js から直接 chrome.scripting.executeScript() を呼べる。

popup.htmlは必ず作るものでは?

popupウィンドウが不要な拡張(ワンクリックで即座に処理するタイプ)ではpopup.htmlを作らず、onClicked + background.js で処理する。


どちらを選ぶかの判断基準

popupなし(background.js + onClicked)を選ぶ場合:

  • クリック後にユーザーとのやりとりが不要
  • 処理が「実行するだけ」で完結する
  • 結果の表示がバッジやアイコンの変化で十分

popupあり(popup.js)を選ぶ場合:

  • 取得した情報をユーザーに見せたい
  • フォームで入力や編集をしてから送信したい
  • 複数の操作ボタンを提供したい

参考リンク

Photo by Pawel Czerwinski on Unsplash