ノート道具を作る

Chrome拡張はどう動くのか――Manifest V3の構成と処理の流れ

manifest.json、Content Script、Service Worker、popupという部品は、どうつながって動くのか。実装でつまずきやすい実行コンテキストの分離、Service Workerが常駐しないこと、メッセージング、権限の設計を、短いコード例とあわせて整理する。

道具を作る 開発記録

Read this note in English

二つの領域に分かれたブラウザウィンドウの中央にパズルピースが置かれ、外側の書類と細い線でつながるミニマルなイラスト

Legacy Toolsの道具は、どれもChrome拡張として作っています。なぜWebサービスではなくChrome拡張にしたのかは以前書きました。今回はその続きとして、Chrome拡張が中でどう動いているのかを、実装の側から整理します。

Chrome拡張は「HTMLとJavaScriptで作れます」と紹介されることが多く、それ自体は正しいのですが、実際に作りはじめてつまずくのは、ファイルの書き方ではなく部品と部品の境界です。JavaScriptがどこで実行されるのか。Service Workerはいつ止まるのか。権限はどこまで及ぶのか。この記事は、その境界に絞って書きます。

ファイル構成から見る

小さな拡張の構成は、おおよそ次のようになります。

my-extension/
├─ manifest.json
├─ background.js
├─ content.js
├─ popup/
│  ├─ popup.html
│  ├─ popup.css
│  └─ popup.js
├─ options/
│  ├─ options.html
│  └─ options.js
└─ icons/
   ├─ icon16.png
   ├─ icon48.png
   └─ icon128.png

各ファイルの役割を、ひとことで書くとこうです。

manifest.json   Chromeに構成・権限・起動条件を伝える
content.js      Webページ上でDOMを監視・操作する
background.js   ブラウザ側のイベント処理を担当する
popup           ツールバーから開く小さな画面
options         継続的な設定を変更する画面

役割が分かれているのは、整理のためだけではありません。それぞれのJavaScriptは別々の場所で実行され、できることも違います。

manifest.jsonがChromeに伝えていること

manifest.jsonは、拡張の構成・権限・起動条件をChromeに伝えるファイルです。短い実例を載せます。

{
  "manifest_version": 3,
  "name": "Example Extension",
  "version": "1.0.0",
  "permissions": ["storage"],
  "host_permissions": ["https://example.com/*"],
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["https://example.com/*"],
      "js": ["content.js"]
    }
  ],
  "action": {
    "default_popup": "popup/popup.html"
  }
}

大事なのは項目の暗記ではなく、項目どうしの関係です。

  • permissionschrome.storageなどのChrome APIを使うための権限
  • host_permissions:どのWebサイトへアクセスできるか
  • content_scripts.matches:どのページでcontent.jsを実行するか
  • background.service_worker:裏側でイベントを処理するファイル
  • action.default_popup:拡張アイコンを押したときに開く画面

permissionshost_permissionsが分かれている点は、あとで書く権限の話につながります。APIを使う権限と、サイトに触る権限は、別のものとして管理されています。

Content ScriptとWebページの境界

Chrome拡張を理解するうえでいちばん重要なのは、ファイル名ではなく、JavaScriptがどこで実行されるかです。

Webページ
├─ ページ自身のJavaScript
└─ Content Script
       ↓ メッセージ
Extension Service Worker
       ↓
chrome.storage / tabs / scripting など

Content ScriptはWebページのDOMを読み書きできます。ただし、ページ本体のJavaScriptとは分離された「isolated world」という環境で動きます。そのため、次のような誤解が起こります。

// Defined by the page's own JavaScript
window.applicationState = {
  loggedIn: true
};
// Not necessarily visible from the content script
console.log(window.applicationState);

DOMは共有しているのに、JavaScriptのグローバル空間は基本的に共有していない。ページのボタンや入力欄には触れるのに、ページのスクリプトが持っている変数はそのまま見えるとは限らない。この非対称を知っているかどうかで、Content Scriptの設計は変わります。

Service Workerは常駐しない

Manifest V3のService Workerを、常駐しているプロセスのように考えると実装を誤ります。

let currentSettings = {};

このようにメモリ上だけへ状態を持っても、Service Workerが停止して再起動すれば消えます。Chromeの公開ドキュメントでは、Service Workerはおおむね30秒間何もしないと停止し、イベントを受け取ると起動し直すと説明されています(2026-07-19時点)。

Service Workerは必要なイベントが発生したときに起動し、処理が終われば停止する可能性がある。メモリ上の変数を、永続的な状態として扱ってはいけない。

残したい値は、chrome.storageへ保存します。

await chrome.storage.local.set({
  enabled: true,
  targetSites: ["example.com"]
});
const settings = await chrome.storage.local.get([
  "enabled",
  "targetSites"
]);

部品どうしのメッセージング

Content ScriptとService Workerは実行環境が別なので、互いの関数を直接呼ぶことはできません。データはメッセージで受け渡します。Content ScriptからService Workerへ処理を頼む例です。

// content.js
const response = await chrome.runtime.sendMessage({
  type: "GET_SETTINGS"
});
// background.js
chrome.runtime.onMessage.addListener(
  (message, sender, sendResponse) => {
    if (message.type === "GET_SETTINGS") {
      chrome.storage.local.get("enabled").then((settings) => {
        sendResponse(settings);
      });

      return true;
    }
  }
);

リスナーの最後にあるreturn trueは、飾りではありません。非同期処理のあとでsendResponseを呼ぶ場合は、文字どおりのtrueを返してメッセージチャネルを開いたままにする必要があります。これを忘れると、応答が届いたり届かなかったりする、原因の分かりにくい不具合になります。

権限も設計の一部

host_permissionsの書き方には、実装の楽さと権限の広さのトレードオフがあります。

  • <all_urls>を最初から要求する。実装の負担は小さいが、すべてのサイトへのアクセスを求める強い権限表示が出て、利用者が不安を感じやすい
  • 対象サイトを限定する。権限を最小にできるが、対応サイトを増やすたびに拡張の更新が必要になる
  • optional_host_permissionsとして宣言し、利用者が必要とするサイトだけ後から許可してもらう。権限を最小に保てるが、許可を求めるUIと権限の管理が複雑になる

どれが常に正しいというものではなく、道具の性質によって決まります。ただ、権限はmanifest.jsonに書く設定項目であると同時に、インストール時に利用者へそのまま表示されるものでもあります。実装の都合だけでは決められません。

Legacy Toolsではどう分けているか

Safe Privacy GateやSafe Attachment Checkがしていることも、簡略化すればこの記事の流れに沿っています。

  1. Content Scriptが送信ボタンやファイル添付を検知する
  2. chrome.storageから利用者の設定を読む
  3. 入力内容や添付ファイルをブラウザの中で確認する
  4. 問題の候補があれば確認ダイアログを出す
  5. 続けるかやめるかは、利用者が決める

責務の分け方も、ここまでに書いた境界に沿わせています。

Content Script
- DOMイベントの監視
- 入力欄や添付操作の検知
- 確認ダイアログの表示

Service Worker
- インストール時の初期化
- 設定やブラウザイベントの管理
- 必要なChrome APIの呼び出し

chrome.storage
- 有効・無効の設定
- 対象サイト
- カスタムルール

小さな拡張でも、境界から考える

この記事で書いたのは、実行コンテキスト、Service Workerのライフサイクル、メッセージング、権限の4つです。小さな拡張でも、この4つの境界は避けて通れません。逆に言えば、ここさえ押さえてしまえば、残りの多くは普通のHTMLとJavaScriptです。

ビルドツールやフレームワーク、テストの話は、今回は書きませんでした。境界の話と混ぜると焦点がぼやけるためで、必要になったら別の記事として書きます。

タグ: Chrome拡張・Manifest V3・開発

← ノート一覧へ