Lecture 1Chrome拡張の仕組み — Manifest V3を理解する

12:00

Chrome拡張の仕組み — Manifest V3を理解する

Chrome拡張機能(Chrome Extension)は、ブラウザに独自の機能を追加できる小さなプログラムです。広告ブロッカー、パスワードマネージャー、翻訳ツールなど、日常的に使われている拡張機能はすべてこの仕組みで動いています。このコースでは、Claude Code を使って「Web Highlighter」というテキストハイライト拡張機能をゼロから構築します。第1回では、Chrome拡張機能の全体アーキテクチャと、最新仕様である Manifest V3 の基本を理解しましょう。

Chrome拡張機能のアーキテクチャ

Chrome拡張機能は、複数のコンポーネントが連携して動作するシステムです。それぞれのコンポーネントは異なる実行環境で動き、メッセージ通信で情報をやり取りします。主要なコンポーネントは以下の5つです。

manifest.json は拡張機能の設計図にあたるファイルです。拡張機能の名前、バージョン、必要な権限、各コンポーネントのファイルパスなどをすべて記述します。Chrome はこのファイルを読み込んで拡張機能を認識します。

ポップアップ(Popup) は、ツールバーの拡張アイコンをクリックしたときに表示される小さなウィンドウです。HTML、CSS、JavaScript で構成され、ユーザーとのインタラクションの入り口になります。Web Highlighter では、ハイライトの ON/OFF 切り替えや保存したハイライトの一覧表示に使います。

コンテンツスクリプト(Content Script) は、ユーザーが閲覧しているWebページに直接注入(inject)される JavaScript です。ページの DOM にアクセスして、テキストの選択検知やハイライトの描画を行います。拡張機能の中で唯一、Webページの内容を操作できるコンポーネントです。

バックグラウンドスクリプト(Service Worker) は、ブラウザのバックグラウンドで動作する処理です。Manifest V3 では Service Worker として実装され、イベント駆動型で動作します。右クリックメニューの登録やタブ間の調整など、裏方の処理を担当します。

オプションページ(Options Page) は、拡張機能の設定画面です。ハイライトの色やキーボードショートカットなど、ユーザーがカスタマイズできる項目を提供します。

Manifest V3 とは何か

Manifest V3 は、Chrome拡張機能の最新仕様です。2024年以降、Chrome Web Store では Manifest V3 のみが受け付けられるようになりました。以前の Manifest V2 からの主な変更点を理解しておきましょう。

Service Worker への移行: Manifest V2 では「バックグラウンドページ」として常時起動するスクリプトが使えましたが、V3 では Service Worker に変わりました。Service Worker はイベントが発生したときだけ起動し、処理が終わると自動的に停止します。これによりメモリ消費が大幅に削減されます。

host_permissions の分離: ウェブサイトへのアクセス権限が permissions から host_permissions に分離されました。これにより、どのサイトにアクセスできるかがユーザーに明確に提示されます。

コンテンツセキュリティの強化: インラインスクリプトやリモートコードの読み込みが制限されました。すべての JavaScript は拡張機能のパッケージ内に含める必要があります。eval()new Function() も使用できません。

宣言的ネットリクエスト: ネットワークリクエストの操作が declarativeNetRequest API に置き換わりました。これは広告ブロッカーなどに影響する変更ですが、Web Highlighter では使いません。

Claude Code にこの仕組みについて聞いてみましょう。

> Chrome拡張機能のManifest V3について教えて。
  V2からの主な変更点と、各コンポーネント
  (popup、content script、service worker)の
  役割を説明して

Claude Code は、V3 の設計思想であるセキュリティとパフォーマンスの向上、そして各コンポーネントの実行コンテキストの違いを詳しく説明してくれます。

Web Highlighter プロジェクトの全体設計

このコースで構築する「Web Highlighter」拡張機能の全体像を確認します。

web-highlighter/
├── manifest.json           # 拡張機能の設計図
├── popup/
│   ├── popup.html          # ポップアップの画面
│   ├── popup.css           # ポップアップのスタイル
│   └── popup.js            # ポップアップのロジック
├── content/
│   └── content.js          # Webページを操作するスクリプト
├── background/
│   └── background.js       # バックグラウンド処理
├── options/
│   ├── options.html         # 設定画面
│   ├── options.css          # 設定画面のスタイル
│   └── options.js           # 設定画面のロジック
└── icons/
    ├── icon-16.png          # ファビコン用
    ├── icon-48.png          # 拡張機能管理画面用
    └── icon-128.png         # Chrome Web Store用

機能一覧: - ユーザーがWebページ上でテキストを選択してハイライトできる - ハイライトの色を複数から選択できる - ハイライトを chrome.storage に保存し、ページ再訪時に復元できる - 右クリックメニューからハイライト操作ができる - オプションページでデフォルトの色やキーバインドを設定できる - ハイライトデータのエクスポート・インポートができる

各講義で1つずつコンポーネントを追加しながら、最終的にすべてが連携する拡張機能を完成させます。

開発環境の準備

Chrome拡張機能の開発には特別なツールは必要ありませんが、以下の環境を整えておきましょう。

Google Chrome: 最新版を使用してください。chrome://version にアクセスすると、現在のバージョンを確認できます。

Claude Code: ターミナルから claude コマンドで起動できることを確認してください。まだインストールしていない場合は、以下のコマンドでインストールします。

npm install -g @anthropic-ai/claude-code

テキストエディタ: VS Code を推奨しますが、Claude Code がファイルの作成・編集を行うので、必須ではありません。

開発者モードの有効化: Chrome で chrome://extensions にアクセスし、右上の「デベロッパーモード」をオンにしてください。これにより、ローカルの拡張機能を読み込めるようになります。

Claude Code を使ってプロジェクトのディレクトリを作成しましょう。

> web-highlighterというChrome拡張機能プロジェクトの
  ディレクトリ構造を作成して。
  popup/、content/、background/、options/、icons/
  フォルダを含めて

Claude Code がディレクトリ構造を一括で作成してくれます。次の講義では、このディレクトリに manifest.json を作成し、Chrome に拡張機能として読み込ませるところまで進めます。

演習問題

  1. Chrome 確認: chrome://extensions にアクセスし、デベロッパーモードを有効にしてください。すでにインストールされている拡張機能の「詳細」を開き、manifest.json の内容を確認してみましょう。
  2. コンポーネント整理: ポップアップ、コンテンツスクリプト、Service Worker の3つのコンポーネントについて、それぞれの実行環境の違いを自分の言葉でまとめてみましょう。
  3. Claude Code 演習: Claude Code を起動し、「Chrome拡張機能で使えるAPIを10個リストアップして」と聞いてみましょう。どのような API が利用可能か確認してください。
  4. V3 調査: Claude Code に「Manifest V2 と V3 の違いを表形式でまとめて」と依頼してみましょう。

参考リンク

Lecture 2プロジェクト作成 — manifest.jsonを書く

12:00

プロジェクト作成 — manifest.jsonを書く

manifest.json は Chrome拡張機能の心臓部です。このファイルがなければ、Chrome は拡張機能を認識できません。拡張機能の名前、バージョン、使用する権限、各コンポーネントのファイルパスなど、すべての設定をこの1つの JSON ファイルに記述します。この講義では、Claude Code を使って Web Highlighter 拡張機能の manifest.json を作成し、Chrome の開発者モードで読み込むところまで進めます。

manifest.json の基本構造

manifest.json に記述する主要なフィールドを理解しましょう。Manifest V3 で必須のフィールドと、Web Highlighter で使用するフィールドを見ていきます。

{
  "manifest_version": 3,
  "name": "Web Highlighter",
  "version": "1.0.0",
  "description": "Webページのテキストをハイライトして保存する拡張機能"
}

manifest_version は必ず 3 を指定します。name は拡張機能の表示名、version はセマンティックバージョニング形式で記述します。description は Chrome Web Store や拡張機能管理画面に表示される説明文です。

これだけでも最小限の拡張機能として成立しますが、Web Highlighter に必要な設定を追加していきましょう。Claude Code に完全な manifest.json を生成してもらいます。

> Chrome拡張機能のmanifest.jsonを作って。
  Manifest V3で、ポップアップ、コンテンツスクリプト、
  バックグラウンドサービスワーカーを含めて。
  名前は "Web Highlighter"、
  説明は "Webページのテキストをハイライトして保存する拡張機能"

Claude Code が生成する manifest.json の全体像は以下のとおりです。

{
  "manifest_version": 3,
  "name": "Web Highlighter",
  "version": "1.0.0",
  "description": "Webページのテキストをハイライトして保存する拡張機能",
  "permissions": [
    "storage",
    "activeTab",
    "contextMenus"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "action": {
    "default_popup": "popup/popup.html",
    "default_icon": {
      "16": "icons/icon-16.png",
      "48": "icons/icon-48.png",
      "128": "icons/icon-128.png"
    }
  },
  "background": {
    "service_worker": "background/background.js"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content/content.js"],
      "css": ["content/content.css"]
    }
  ],
  "options_page": "options/options.html",
  "icons": {
    "16": "icons/icon-16.png",
    "48": "icons/icon-48.png",
    "128": "icons/icon-128.png"
  }
}

各フィールドの詳細解説

生成された manifest.json の各セクションを詳しく見ていきましょう。

permissions: 拡張機能が必要とする Chrome API へのアクセス権限を宣言します。 - storagechrome.storage API を使ってデータを保存する - activeTab — ユーザーがアクションを起こしたときにアクティブなタブにアクセスする - contextMenus — 右クリックメニューに独自の項目を追加する

host_permissions: どのウェブサイトにコンテンツスクリプトを注入できるかを指定します。<all_urls> はすべてのサイトを意味しますが、本番環境ではできるだけ制限することが推奨されます。

action: Manifest V3 では browser_actionpage_action が統合されて action になりました。default_popup でポップアップの HTML ファイルを、default_icon でツールバーアイコンを指定します。

background: Service Worker のファイルパスを指定します。V2 の background.scriptsbackground.page とは記述方法が異なることに注意してください。

content_scripts: matches パターンに一致するページに自動的に注入されるスクリプトと CSS を指定します。jscss を配列で渡せるため、複数のファイルを注入することも可能です。

アイコンを用意する

Chrome拡張機能には3つのサイズのアイコンが必要です。Claude Code にアイコンのプレースホルダーを作成してもらいましょう。

> Web Highlighter拡張のアイコン用にプレースホルダーの
  SVGファイルを作成して。黄色のマーカーペンのデザインで。
  16x16、48x48、128x128の3サイズが必要

実際のChrome拡張機能には PNG 形式のアイコンが必要です。開発段階では、シンプルな画像を使うか、オンラインのアイコンジェネレーターで作成できます。ここでは開発用に最小限のアイコンを作りましょう。

Claude Code に HTML Canvas を使ったアイコン生成スクリプトを作ってもらう方法もあります。

> Node.jsのcanvasパッケージを使って、
  黄色い円のシンプルなPNGアイコンを
  3サイズ(16, 48, 128)生成するスクリプトを書いて

あるいは、開発中はフリーのアイコン素材サイトからダウンロードして使っても構いません。アイコンがないと manifest.json の読み込み時にエラーが出ることがあるため、何らかの画像ファイルを配置しておくことが重要です。

Chrome に拡張機能を読み込む

manifest.json とアイコンの準備ができたら、Chrome に拡張機能を読み込みましょう。

  1. Chrome で chrome://extensions を開く
  2. 右上の「デベロッパーモード」がオンになっていることを確認する
  3. 「パッケージ化されていない拡張機能を読み込む」をクリックする
  4. web-highlighter フォルダを選択する

読み込みが成功すると、拡張機能の一覧に「Web Highlighter」が表示されます。ツールバーにもアイコンが追加されます。

まだポップアップやコンテンツスクリプトのファイルを作成していないため、アイコンをクリックしてもエラーが表示されます。これは正常です。次の講義でポップアップ UI を作成すれば解決します。

エラーが出た場合は、Claude Code に相談しましょう。

> Chrome拡張機能を読み込んだらエラーが出た
  "Could not load manifest" と表示される。
  原因を教えて

よくあるエラーは以下のとおりです。 - JSON の構文エラー(カンマの過不足、引用符の不一致) - 指定したファイルパスにファイルが存在しない - manifest_version が 3 以外の値になっている - 必須フィールド(name, version, manifest_version)が欠けている

コンポーネントの空ファイルを作成する

Chrome がエラーを出さないよう、各コンポーネントの最小限のファイルを作成しておきましょう。

> manifest.jsonで指定した各ファイルの空テンプレートを
  作成して。popup.html、popup.js、popup.css、
  content.js、content.css、background.js、
  options.html、options.js、options.css
  それぞれ最小限の内容で

Claude Code が生成する popup/popup.html の最小テンプレートです。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Web Highlighter</title>
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <h1>Web Highlighter</h1>
  <script src="popup.js"></script>
</body>
</html>

background/background.js の最小テンプレートです。

// Service Worker
console.log('Web Highlighter background service worker started');

content/content.js の最小テンプレートです。

// Content Script
console.log('Web Highlighter content script loaded');

これらのファイルを配置した状態で再度 chrome://extensions から拡張機能をリロード(更新アイコンをクリック)すると、エラーなく読み込めるはずです。

演習問題

  1. manifest.json 作成: Claude Code を使って manifest.json を生成し、web-highlighter フォルダに保存してください。Chrome の開発者モードで正常に読み込めることを確認しましょう。
  2. 権限の理解: permissionstabs を追加したらどうなるか、Claude Code に聞いてみましょう。プロンプト例: Chrome拡張のtabs権限で何ができるか教えて
  3. matches パターン: content_scripts の matches を ["https://*.github.com/*"] に変更して、GitHub のページだけでコンテンツスクリプトが動作するか確認してください。コンソール(F12)にログが表示されることを確認します。
  4. エラーデバッグ: manifest.json から manifest_version フィールドを意図的に削除して、Chrome がどのようなエラーを表示するか確認してください。その後、元に戻してください。

参考リンク

Lecture 3ポップアップUI — 拡張のメイン画面を作る

12:00

ポップアップUI — 拡張のメイン画面を作る

ポップアップは、ユーザーが拡張機能のアイコンをクリックしたときに表示される小さなウィンドウです。Web Highlighter のポップアップでは、ハイライト機能の ON/OFF 切り替え、ハイライト色の選択、保存済みハイライトの件数表示を行います。この講義では、Claude Code を使って見た目も使いやすさも備えたポップアップ UI を構築していきましょう。

ポップアップの特徴と制約

ポップアップには、通常の Web ページとは異なるいくつかの特徴と制約があります。開発前にこれらを理解しておくことが重要です。

サイズの制約: ポップアップのデフォルトサイズは内容に合わせて自動調整されますが、最大幅は 800px、最大高さは 600px です。一般的には幅 300〜400px、高さ 400〜500px 程度に収めるのが良いデザインです。

ライフサイクル: ポップアップはアイコンをクリックするたびに開き、フォーカスが外れると閉じます。閉じると JavaScript の状態はすべてリセットされます。そのため、状態の永続化には chrome.storage を使う必要があります。

セキュリティ制限: ポップアップ内でインラインスクリプト(<script> タグ内に直接 JavaScript を書くこと)は許可されていません。必ず外部ファイルとしてスクリプトを読み込みます。また、onclick などのインラインイベントハンドラも使えません。すべてのイベントリスナーは JavaScript ファイル内で addEventListener を使って登録します。

デバッグ方法: ポップアップを右クリックして「検証」を選ぶと、ポップアップ専用の DevTools が開きます。コンソールでエラーを確認したり、要素のスタイルを調整したりできます。

popup.html を作成する

Claude Code にポップアップの HTML を生成してもらいましょう。

> Web Highlighter拡張のpopup.htmlを作って。
  以下の要素を含めて:
  - タイトル「Web Highlighter」
  - ハイライトのON/OFFトグルスイッチ
  - ハイライト色の選択ボタン(黄・青・緑・ピンク)
  - 現在のページのハイライト件数の表示
  - 「すべてクリア」ボタン
  インラインスクリプトは使わないこと

Claude Code が生成する popup.html は以下のようになります。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Web Highlighter</title>
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <div class="container">
    <header class="header">
      <h1 class="title">Web Highlighter</h1>
      <div class="toggle-wrapper">
        <label class="toggle">
          <input type="checkbox" id="toggle-highlight" checked>
          <span class="toggle-slider"></span>
        </label>
        <span id="toggle-label">ON</span>
      </div>
    </header>

    <section class="color-picker">
      <p class="section-label">ハイライト色</p>
      <div class="color-buttons">
        <button class="color-btn active" data-color="#FFEB3B" style="background-color: #FFEB3B;" title="黄色"></button>
        <button class="color-btn" data-color="#64B5F6" style="background-color: #64B5F6;" title="青"></button>
        <button class="color-btn" data-color="#81C784" style="background-color: #81C784;" title="緑"></button>
        <button class="color-btn" data-color="#F48FB1" style="background-color: #F48FB1;" title="ピンク"></button>
      </div>
    </section>

    <section class="stats">
      <div class="stat-item">
        <span class="stat-label">このページのハイライト</span>
        <span class="stat-value" id="highlight-count">0</span>
      </div>
    </section>

    <footer class="footer">
      <button id="clear-all" class="btn btn-danger">すべてクリア</button>
      <button id="open-options" class="btn btn-secondary">設定</button>
    </footer>
  </div>

  <script src="popup.js"></script>
</body>
</html>

各要素の iddata-* 属性は、JavaScript から操作するために設定しています。data-color 属性にはカラーコードを持たせており、クリックされたボタンの色を取得するのに使います。

popup.css でスタイリングする

次にポップアップの見た目を整えます。

> popup.cssを作って。モダンで清潔感のある
  デザインにして。幅は320px。
  トグルスイッチはiOS風のデザイン。
  カラーボタンは丸い形で、選択中は
  ボーダーで強調して

Claude Code が生成する CSS です。

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  width: 320px;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background-color: #ffffff;
  color: #333;
}

.container {
  padding: 16px;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 16px;
  padding-bottom: 12px;
  border-bottom: 1px solid #e0e0e0;
}

.title {
  font-size: 16px;
  font-weight: 600;
  color: #1a1a1a;
}

.toggle-wrapper {
  display: flex;
  align-items: center;
  gap: 8px;
}

.toggle {
  position: relative;
  width: 44px;
  height: 24px;
}

.toggle input {
  opacity: 0;
  width: 0;
  height: 0;
}

.toggle-slider {
  position: absolute;
  cursor: pointer;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: #ccc;
  border-radius: 24px;
  transition: 0.3s;
}

.toggle-slider::before {
  content: "";
  position: absolute;
  height: 18px;
  width: 18px;
  left: 3px;
  bottom: 3px;
  background-color: white;
  border-radius: 50%;
  transition: 0.3s;
}

.toggle input:checked + .toggle-slider {
  background-color: #4CAF50;
}

.toggle input:checked + .toggle-slider::before {
  transform: translateX(20px);
}

.section-label {
  font-size: 12px;
  color: #666;
  margin-bottom: 8px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
}

.color-buttons {
  display: flex;
  gap: 8px;
  margin-bottom: 16px;
}

.color-btn {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 3px solid transparent;
  cursor: pointer;
  transition: border-color 0.2s, transform 0.2s;
}

.color-btn:hover {
  transform: scale(1.1);
}

.color-btn.active {
  border-color: #333;
}

.stats {
  background-color: #f5f5f5;
  border-radius: 8px;
  padding: 12px;
  margin-bottom: 16px;
}

.stat-item {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.stat-label {
  font-size: 13px;
  color: #555;
}

.stat-value {
  font-size: 20px;
  font-weight: 700;
  color: #1a1a1a;
}

.footer {
  display: flex;
  gap: 8px;
}

.btn {
  flex: 1;
  padding: 8px 12px;
  border: none;
  border-radius: 6px;
  font-size: 13px;
  cursor: pointer;
  transition: background-color 0.2s;
}

.btn-danger {
  background-color: #ef5350;
  color: white;
}

.btn-danger:hover {
  background-color: #d32f2f;
}

.btn-secondary {
  background-color: #e0e0e0;
  color: #333;
}

.btn-secondary:hover {
  background-color: #bdbdbd;
}

popup.js でインタラクションを実装する

最後に、ポップアップの動的な振る舞いを実装します。

> popup.jsを書いて。以下の機能を実装して:
  - トグルスイッチでハイライトON/OFFを切り替え、
    状態をchrome.storageに保存
  - カラーボタンクリックで選択色を変更し、
    activeクラスを切り替え
  - ページ読み込み時にchrome.storageから
    設定を復元
  - 「すべてクリア」ボタンでcontent scriptに
    クリアメッセージを送信
  - 設定ボタンでオプションページを開く

Claude Code が生成する popup.js です。

document.addEventListener('DOMContentLoaded', () => {
  const toggle = document.getElementById('toggle-highlight');
  const toggleLabel = document.getElementById('toggle-label');
  const colorButtons = document.querySelectorAll('.color-btn');
  const highlightCount = document.getElementById('highlight-count');
  const clearAllBtn = document.getElementById('clear-all');
  const openOptionsBtn = document.getElementById('open-options');

  // 保存された設定を復元
  chrome.storage.local.get(['isEnabled', 'highlightColor'], (result) => {
    const isEnabled = result.isEnabled !== undefined ? result.isEnabled : true;
    const color = result.highlightColor || '#FFEB3B';

    toggle.checked = isEnabled;
    toggleLabel.textContent = isEnabled ? 'ON' : 'OFF';

    colorButtons.forEach(btn => {
      btn.classList.toggle('active', btn.dataset.color === color);
    });
  });

  // ハイライト件数を取得
  chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
    if (tabs[0]) {
      chrome.tabs.sendMessage(tabs[0].id, { action: 'getCount' }, (response) => {
        if (response && response.count !== undefined) {
          highlightCount.textContent = response.count;
        }
      });
    }
  });

  // トグル切り替え
  toggle.addEventListener('change', () => {
    const isEnabled = toggle.checked;
    toggleLabel.textContent = isEnabled ? 'ON' : 'OFF';
    chrome.storage.local.set({ isEnabled });

    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
      if (tabs[0]) {
        chrome.tabs.sendMessage(tabs[0].id, {
          action: 'toggleHighlight',
          enabled: isEnabled
        });
      }
    });
  });

  // 色選択
  colorButtons.forEach(btn => {
    btn.addEventListener('click', () => {
      colorButtons.forEach(b => b.classList.remove('active'));
      btn.classList.add('active');
      const color = btn.dataset.color;
      chrome.storage.local.set({ highlightColor: color });
    });
  });

  // すべてクリア
  clearAllBtn.addEventListener('click', () => {
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
      if (tabs[0]) {
        chrome.tabs.sendMessage(tabs[0].id, { action: 'clearAll' });
        highlightCount.textContent = '0';
      }
    });
  });

  // オプションページを開く
  openOptionsBtn.addEventListener('click', () => {
    chrome.runtime.openOptionsPage();
  });
});

すべてのイベントリスナーは addEventListener で登録しており、Manifest V3 の CSP(Content Security Policy)に準拠しています。chrome.storage.localchrome.tabs.sendMessage は後続の講義で詳しく解説します。

演習問題

  1. UI カスタマイズ: Claude Code に「ポップアップにダークモード切り替えボタンを追加して」と依頼し、CSS と JS を拡張してみましょう。
  2. 色追加: カラーボタンに「オレンジ」と「紫」を追加してください。HTML に <button> を追加し、CSS で色を指定します。
  3. デバッグ: ポップアップを開いて右クリック → 「検証」で DevTools を開き、Console タブでエラーが出ていないか確認してください。
  4. レイアウト変更: Claude Code に「カラーボタンをグリッドレイアウト(2x3)に変更して」と依頼し、CSS Grid を使った別のレイアウトを試してみましょう。

参考リンク

Lecture 4コンテンツスクリプト — Webページを操作する

12:00

コンテンツスクリプト — Webページを操作する

コンテンツスクリプトは、Chrome拡張機能の中で最も強力なコンポーネントです。ユーザーが閲覧しているWebページの DOM に直接アクセスし、要素の追加・変更・削除を行えます。Web Highlighter の核心機能であるテキストのハイライト処理は、すべてこのコンテンツスクリプトで実装します。この講義では、テキスト選択の検知、ハイライトの描画、ハイライトの削除まで、コンテンツスクリプトの主要な機能を Claude Code で一気に構築していきましょう。

コンテンツスクリプトの実行環境

コンテンツスクリプトは特殊な実行環境で動作します。この環境を「Isolated World(分離された世界)」と呼びます。

DOM は共有、JavaScript は分離: コンテンツスクリプトはWebページの DOM ツリーにアクセスできますが、ページ側の JavaScript 変数や関数にはアクセスできません。逆に、ページ側のスクリプトもコンテンツスクリプトの変数にはアクセスできません。これにより、拡張機能とWebページが互いに干渉することを防いでいます。

利用可能な Chrome API: コンテンツスクリプトでは、chrome.runtime(メッセージ通信)、chrome.storage(データ保存)、chrome.i18n(国際化)など、限られた API のみが使えます。chrome.tabschrome.contextMenus は使えないため、これらはバックグラウンドスクリプトに任せます。

注入タイミング: manifest.json の content_scripts セクションで run_at を指定することで、スクリプトの注入タイミングを制御できます。 - document_start — DOM の構築前に注入 - document_idle(デフォルト)— DOM の構築後、画像などの読み込み完了前に注入 - document_end — DOMContentLoaded イベントの直後に注入

Web Highlighter では、ページの DOM が完成してからハイライトを復元する必要があるため、デフォルトの document_idle が適切です。

テキスト選択を検知する

まず、ユーザーがWebページ上でテキストを選択したことを検知する機能を実装します。

> content.jsを書いて。
  ユーザーがWebページでテキストを選択したときに
  ハイライトする機能を実装して。
  mouseupイベントでテキスト選択を検知し、
  選択範囲にハイライト用のspanタグを挿入して。
  ハイライト色はchrome.storageから取得して

Claude Code が生成する content.js のテキスト選択検知部分です。

// ハイライトの状態管理
let isEnabled = true;
let highlightColor = '#FFEB3B';
let highlights = [];

// 設定を読み込む
chrome.storage.local.get(['isEnabled', 'highlightColor'], (result) => {
  isEnabled = result.isEnabled !== undefined ? result.isEnabled : true;
  highlightColor = result.highlightColor || '#FFEB3B';
});

// 設定変更を監視
chrome.storage.onChanged.addListener((changes, area) => {
  if (area === 'local') {
    if (changes.isEnabled) {
      isEnabled = changes.isEnabled.newValue;
    }
    if (changes.highlightColor) {
      highlightColor = changes.highlightColor.newValue;
    }
  }
});

// テキスト選択を検知
document.addEventListener('mouseup', (e) => {
  if (!isEnabled) return;

  const selection = window.getSelection();
  if (!selection || selection.isCollapsed || selection.toString().trim() === '') {
    return;
  }

  // 既にハイライトされた要素内の選択は無視
  const anchorNode = selection.anchorNode;
  if (anchorNode && anchorNode.parentElement &&
      anchorNode.parentElement.classList.contains('web-highlighter-mark')) {
    return;
  }

  applyHighlight(selection);
});

window.getSelection() でユーザーの選択範囲を取得し、selection.isCollapsed で何も選択されていない状態(カーソルが点滅しているだけの状態)を除外しています。selection.toString().trim() で空白のみの選択も除外します。

ハイライトを描画する

選択されたテキストにハイライトを適用する関数を実装します。ここが Web Highlighter の最も重要な処理です。

> applyHighlight関数を実装して。
  Selection APIのRangeオブジェクトを使って、
  選択テキストをハイライト用のspan要素で囲む。
  一意のIDを付与して、ハイライト情報を
  配列に保存して
function applyHighlight(selection) {
  const range = selection.getRangeAt(0);
  const highlightId = 'hl-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);

  // ハイライト用のspan要素を作成
  const highlightSpan = document.createElement('span');
  highlightSpan.className = 'web-highlighter-mark';
  highlightSpan.dataset.highlightId = highlightId;
  highlightSpan.style.backgroundColor = highlightColor;
  highlightSpan.style.cursor = 'pointer';

  try {
    // 選択範囲をspanで囲む
    range.surroundContents(highlightSpan);
  } catch (e) {
    // 複数のDOM要素にまたがる選択の場合のフォールバック
    const fragment = range.extractContents();
    highlightSpan.appendChild(fragment);
    range.insertNode(highlightSpan);
  }

  // ハイライト情報を記録
  const highlightData = {
    id: highlightId,
    text: selection.toString(),
    color: highlightColor,
    url: window.location.href,
    timestamp: Date.now(),
    xpath: getXPath(highlightSpan),
    textOffset: getTextOffset(highlightSpan)
  };

  highlights.push(highlightData);
  saveHighlights();

  // 選択を解除
  selection.removeAllRanges();

  // ハイライトにクリックイベントを追加(削除用)
  highlightSpan.addEventListener('click', (e) => {
    if (e.altKey) {
      removeHighlight(highlightId);
    }
  });
}

range.surroundContents() は、選択範囲が単一の DOM 要素内に収まっている場合にうまく動作します。しかし、選択が <p> タグをまたいでいるような場合はエラーが発生するため、try-catch でフォールバック処理を行っています。

ハイライトの削除と位置の特定

ハイライトを削除する機能と、DOM 内の位置を特定するユーティリティ関数を実装します。

> removeHighlight関数、getXPath関数、
  getTextOffset関数を実装して。
  ハイライト削除時はspanを除去して
  テキストノードを元に戻す処理にして
function removeHighlight(highlightId) {
  const span = document.querySelector(`[data-highlight-id="${highlightId}"]`);
  if (!span) return;

  // spanの中身をテキストノードとして取り出す
  const parent = span.parentNode;
  while (span.firstChild) {
    parent.insertBefore(span.firstChild, span);
  }
  parent.removeChild(span);

  // 隣接するテキストノードを結合
  parent.normalize();

  // 配列から削除
  highlights = highlights.filter(h => h.id !== highlightId);
  saveHighlights();
}

function getXPath(element) {
  const parts = [];
  let current = element;

  while (current && current.nodeType === Node.ELEMENT_NODE) {
    let index = 1;
    let sibling = current.previousSibling;
    while (sibling) {
      if (sibling.nodeType === Node.ELEMENT_NODE &&
          sibling.tagName === current.tagName) {
        index++;
      }
      sibling = sibling.previousSibling;
    }
    parts.unshift(`${current.tagName.toLowerCase()}[${index}]`);
    current = current.parentNode;
  }

  return '/' + parts.join('/');
}

function getTextOffset(element) {
  const treeWalker = document.createTreeWalker(
    document.body,
    NodeFilter.SHOW_TEXT,
    null,
    false
  );

  let offset = 0;
  let node;
  while (node = treeWalker.nextNode()) {
    if (node.parentElement === element || element.contains(node)) {
      return offset;
    }
    offset += node.textContent.length;
  }
  return offset;
}

removeHighlight 関数では、ハイライト用の <span> を取り除いた後、parent.normalize() を呼んで隣接するテキストノードを1つに結合しています。これを行わないと、同じテキストに再度ハイライトを適用したときに意図しない動作が起こる可能性があります。

メッセージリスナーを実装する

ポップアップやバックグラウンドスクリプトからのメッセージを受け取るリスナーを追加します。

// メッセージリスナー
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  switch (message.action) {
    case 'toggleHighlight':
      isEnabled = message.enabled;
      break;

    case 'getCount':
      sendResponse({ count: highlights.length });
      break;

    case 'clearAll':
      highlights.forEach(h => {
        const span = document.querySelector(`[data-highlight-id="${h.id}"]`);
        if (span) {
          const parent = span.parentNode;
          while (span.firstChild) {
            parent.insertBefore(span.firstChild, span);
          }
          parent.removeChild(span);
          parent.normalize();
        }
      });
      highlights = [];
      saveHighlights();
      sendResponse({ success: true });
      break;
  }
  return true;
});

// ハイライトをストレージに保存
function saveHighlights() {
  const url = window.location.href;
  chrome.storage.local.get(['allHighlights'], (result) => {
    const allHighlights = result.allHighlights || {};
    allHighlights[url] = highlights;
    chrome.storage.local.set({ allHighlights });
  });
}

return true をリスナーの最後に記述することで、非同期的に sendResponse を呼べるようになります。これを忘れると、非同期処理の結果を返す前にメッセージチャネルが閉じてしまいます。

演習問題

  1. ハイライト実装: Claude Code を使って content.js を完成させ、実際のWebページでテキストをハイライトできることを確認してください。
  2. ダブルクリック対応: Claude Code に「ダブルクリックで単語を自動選択してハイライトする機能を追加して」と依頼してみましょう。dblclick イベントを使います。
  3. ハイライトスタイル: content.css を作成して、ハイライトにアニメーション効果(ふわっと表示される等)を追加してみましょう。
  4. 複数要素対応: <p> タグをまたぐテキスト選択を試してみましょう。フォールバック処理が正しく動作するか確認してください。

参考リンク

Lecture 5Chrome Storage API — データを保存する

12:00

Chrome Storage API — データを保存する

Web Highlighter のハイライトデータを永続的に保存するには、Chrome Storage API を使います。通常の Web 開発では localStorage を使いますが、Chrome拡張機能では chrome.storage API が推奨されています。この講義では、Chrome Storage API の種類と使い分け、データの保存・読み込み・削除の実装、そしてストレージ変更の監視まで、Claude Code を使って包括的に実装していきましょう。

chrome.storage と localStorage の違い

なぜ Chrome拡張機能では localStorage ではなく chrome.storage を使うべきなのでしょうか。両者には重要な違いがあります。

スコープの違い: localStorage はオリジン(ドメイン)ごとに分離されています。コンテンツスクリプトで localStorage を使うと、そのWebページのオリジンのストレージにアクセスすることになり、拡張機能専用のデータ領域にはなりません。一方、chrome.storage は拡張機能専用の領域で、どのコンポーネント(ポップアップ、コンテンツスクリプト、Service Worker)からも同じデータにアクセスできます。

非同期 vs 同期: localStorage は同期 API で、大量のデータ操作時にメインスレッドをブロックする可能性があります。chrome.storage は非同期 API で、パフォーマンスに優れています。

容量の違い: localStorage は約 5MB の制限がありますが、chrome.storage.local はデフォルトで 10MB、unlimitedStorage 権限を追加すれば無制限に使えます。

同期機能: chrome.storage.sync を使えば、同じ Google アカウントでログインしている複数のデバイス間でデータを自動同期できます。

Claude Code にこの違いを聞いてみましょう。

> Chrome拡張機能でlocalStorageではなく
  chrome.storageを使うべき理由を
  コード例付きで教えて

chrome.storage の3つの種類

Chrome Storage API には3つのストレージ領域があります。用途に応じて使い分けます。

chrome.storage.local: ローカルマシンにのみ保存されるデータです。容量は 10MB(unlimitedStorage 権限で無制限)。ハイライトデータのように大量になる可能性があるデータに適しています。

chrome.storage.sync: Google アカウントで同期されるデータです。容量は合計 100KB、1アイテムあたり 8KB まで。拡張機能の設定(ハイライト色のデフォルト値やキーバインド)のように少量で、複数デバイスで共有したいデータに適しています。

chrome.storage.session: ブラウザセッション中のみ保持される一時データです。Manifest V3 で追加されました。ブラウザを閉じると消えます。一時的なフラグや、Service Worker と他のコンポーネント間の短期的な状態共有に使います。

Web Highlighter では以下のように使い分けます。

データ ストレージ 理由
ハイライトデータ chrome.storage.local 大量データ、ローカル保持
設定(デフォルト色等) chrome.storage.sync 少量、デバイス間同期
一時的な選択状態 chrome.storage.session セッション中のみ必要

データの保存と読み込み

Claude Code にストレージ操作のユーティリティモジュールを作成してもらいましょう。

> Web Highlighter用のストレージユーティリティを
  作って。以下の関数を含めて:
  - saveHighlight(url, highlightData)
  - getHighlights(url)
  - removeHighlight(url, highlightId)
  - clearHighlights(url)
  - getAllHighlights()
  chrome.storage.localを使って、
  URLごとにハイライトを管理して

Claude Code が生成するストレージユーティリティです。

// storage.js - ストレージユーティリティ

const StorageUtil = {
  // ハイライトを保存
  saveHighlight(url, highlightData) {
    return new Promise((resolve) => {
      chrome.storage.local.get(['highlights'], (result) => {
        const highlights = result.highlights || {};
        if (!highlights[url]) {
          highlights[url] = [];
        }
        highlights[url].push(highlightData);
        chrome.storage.local.set({ highlights }, resolve);
      });
    });
  },

  // 特定URLのハイライトを取得
  getHighlights(url) {
    return new Promise((resolve) => {
      chrome.storage.local.get(['highlights'], (result) => {
        const highlights = result.highlights || {};
        resolve(highlights[url] || []);
      });
    });
  },

  // 特定のハイライトを削除
  removeHighlight(url, highlightId) {
    return new Promise((resolve) => {
      chrome.storage.local.get(['highlights'], (result) => {
        const highlights = result.highlights || {};
        if (highlights[url]) {
          highlights[url] = highlights[url].filter(h => h.id !== highlightId);
          if (highlights[url].length === 0) {
            delete highlights[url];
          }
        }
        chrome.storage.local.set({ highlights }, resolve);
      });
    });
  },

  // 特定URLのハイライトをすべてクリア
  clearHighlights(url) {
    return new Promise((resolve) => {
      chrome.storage.local.get(['highlights'], (result) => {
        const highlights = result.highlights || {};
        delete highlights[url];
        chrome.storage.local.set({ highlights }, resolve);
      });
    });
  },

  // 全URLのハイライトを取得
  getAllHighlights() {
    return new Promise((resolve) => {
      chrome.storage.local.get(['highlights'], (result) => {
        resolve(result.highlights || {});
      });
    });
  },

  // ストレージ使用量を取得
  getStorageUsage() {
    return new Promise((resolve) => {
      chrome.storage.local.getBytesInUse(null, (bytes) => {
        resolve({
          used: bytes,
          total: chrome.storage.local.QUOTA_BYTES,
          percentage: ((bytes / chrome.storage.local.QUOTA_BYTES) * 100).toFixed(2)
        });
      });
    });
  }
};

各関数は Promise を返すように設計しています。これにより、async/await 構文で直感的に使えます。

// 使用例
async function example() {
  // ハイライトを保存
  await StorageUtil.saveHighlight('https://example.com', {
    id: 'hl-001',
    text: 'ハイライトされたテキスト',
    color: '#FFEB3B',
    timestamp: Date.now()
  });

  // ハイライトを取得
  const highlights = await StorageUtil.getHighlights('https://example.com');
  console.log(`${highlights.length}件のハイライトがあります`);

  // ストレージ使用量を確認
  const usage = await StorageUtil.getStorageUsage();
  console.log(`使用量: ${usage.percentage}%`);
}

ストレージ変更の監視

chrome.storage.onChanged イベントを使うと、ストレージの値が変更されたときにリアルタイムで通知を受け取れます。これにより、ポップアップで設定を変更したときにコンテンツスクリプトが即座に反応できます。

> chrome.storage.onChangedリスナーを実装して。
  設定変更時にコンテンツスクリプトが
  リアルタイムで反応するようにして
// 設定変更の監視(content.js内)
chrome.storage.onChanged.addListener((changes, areaName) => {
  if (areaName !== 'local') return;

  // ハイライト色の変更
  if (changes.highlightColor) {
    highlightColor = changes.highlightColor.newValue;
    console.log(`ハイライト色が ${highlightColor} に変更されました`);
  }

  // ON/OFF 状態の変更
  if (changes.isEnabled) {
    isEnabled = changes.isEnabled.newValue;
    console.log(`ハイライト機能: ${isEnabled ? 'ON' : 'OFF'}`);
  }

  // 設定の同期変更を監視
  if (changes.settings) {
    const newSettings = changes.settings.newValue;
    applySettings(newSettings);
  }
});

function applySettings(settings) {
  if (settings.defaultColor) {
    highlightColor = settings.defaultColor;
  }
  if (settings.opacity !== undefined) {
    document.querySelectorAll('.web-highlighter-mark').forEach(el => {
      el.style.opacity = settings.opacity;
    });
  }
}

changes オブジェクトには、変更されたキーごとに oldValuenewValue が含まれています。これを使って、変更前後の値を比較したり、新しい値を即座に適用したりできます。

ページ再訪時のハイライト復元

ユーザーがハイライトしたページを再度訪問したときに、保存されたハイライトを復元する機能を実装します。

// ページ読み込み時にハイライトを復元
async function restoreHighlights() {
  const url = window.location.href;
  const savedHighlights = await StorageUtil.getHighlights(url);

  if (savedHighlights.length === 0) return;

  console.log(`${savedHighlights.length}件のハイライトを復元中...`);

  savedHighlights.forEach(data => {
    try {
      restoreSingleHighlight(data);
    } catch (e) {
      console.warn(`ハイライト復元失敗: ${data.id}`, e);
    }
  });

  highlights = savedHighlights;
}

function restoreSingleHighlight(data) {
  // テキスト検索による復元
  const treeWalker = document.createTreeWalker(
    document.body,
    NodeFilter.SHOW_TEXT,
    null,
    false
  );

  let node;
  while (node = treeWalker.nextNode()) {
    const index = node.textContent.indexOf(data.text);
    if (index !== -1) {
      const range = document.createRange();
      range.setStart(node, index);
      range.setEnd(node, index + data.text.length);

      const span = document.createElement('span');
      span.className = 'web-highlighter-mark';
      span.dataset.highlightId = data.id;
      span.style.backgroundColor = data.color;
      span.style.cursor = 'pointer';

      range.surroundContents(span);

      span.addEventListener('click', (e) => {
        if (e.altKey) removeHighlight(data.id);
      });
      return;
    }
  }
}

// ページ読み込み完了後に実行
restoreHighlights();

テキスト検索による復元は単純ですが、ページの内容が変更された場合にうまく動作しないことがあります。より堅牢な実装には、XPath やテキストオフセットを組み合わせた位置特定が必要です。これは発展的な課題として取り組んでみてください。

演習問題

  1. ストレージ確認: Chrome の DevTools で chrome.storage.local.get(null, console.log) を実行して、保存されているデータの構造を確認してください。
  2. sync ストレージ: Claude Code に「設定データを chrome.storage.sync に保存する関数を追加して」と依頼し、sync ストレージの実装も体験してみましょう。
  3. 容量監視: getStorageUsage() を使って、ハイライトデータがストレージ容量の何パーセントを使っているか、ポップアップに表示する機能を追加してください。
  4. エクスポート機能: Claude Code に「すべてのハイライトデータをJSON形式でダウンロードするエクスポート機能を作って」と依頼してみましょう。

参考リンク

Lecture 6バックグラウンドスクリプト — 常駐処理を実装する

12:00

バックグラウンドスクリプト — 常駐処理を実装する

バックグラウンドスクリプトは、Chrome拡張機能の「司令塔」として機能するコンポーネントです。Manifest V3 では Service Worker として実装され、イベント駆動型で動作します。ユーザーの操作がなくても裏側で処理を実行し、右クリックメニューの管理、タブの監視、アラーム処理などを担当します。この講義では、Web Highlighter のバックグラウンド処理として、コンテキストメニュー(右クリックメニュー)の実装とイベント処理を Claude Code で構築していきましょう。

Service Worker の特徴

Manifest V3 のバックグラウンドスクリプトは Service Worker として実装されます。Manifest V2 の常駐型バックグラウンドページとは大きく異なる性質を持っています。

イベント駆動型: Service Worker は常時起動しているわけではありません。登録されたイベント(メッセージ受信、アラーム、タブ変更など)が発生したときだけ起動し、処理が完了すると自動的に停止します。これにより、メモリ消費が大幅に削減されます。

DOM にアクセスできない: Service Worker は documentwindow オブジェクトにアクセスできません。DOM 操作が必要な場合は、コンテンツスクリプトにメッセージを送って処理を委任します。

状態の非永続性: Service Worker が停止すると、グローバル変数の値はリセットされます。状態の永続化には必ず chrome.storage を使う必要があります。chrome.storage.session を使えば、ブラウザセッション中のみ有効な一時データを Service Worker 間で共有できます。

ライフサイクル: Service Worker は最大5分間アクティブでいられます。長時間の処理が必要な場合は、chrome.alarms API を使って定期的にウェイクアップさせる仕組みが必要です。

Claude Code にこの仕組みについて質問してみましょう。

> Chrome拡張のService Workerの
  ライフサイクルについて詳しく教えて。
  いつ起動して、いつ停止するのか。
  状態を保持する方法も含めて

コンテキストメニューの実装

Web Highlighter に右クリックメニューを追加します。テキストを選択した状態で右クリックすると、「ハイライトを追加」メニューが表示されるようにします。

> background.jsを書いて。
  右クリックメニューに「ハイライトを追加」と
  「ハイライトを削除」の項目を追加して。
  テキスト選択時のみ表示されるようにして。
  chrome.contextMenus APIを使って

Claude Code が生成する background.js のコンテキストメニュー部分です。

// background.js - Service Worker

// 拡張機能インストール時にコンテキストメニューを作成
chrome.runtime.onInstalled.addListener((details) => {
  console.log('Web Highlighter installed:', details.reason);

  // コンテキストメニューの作成
  chrome.contextMenus.create({
    id: 'highlight-selection',
    title: 'テキストをハイライト',
    contexts: ['selection']
  });

  chrome.contextMenus.create({
    id: 'highlight-yellow',
    title: '黄色でハイライト',
    parentId: 'highlight-selection',
    contexts: ['selection']
  });

  chrome.contextMenus.create({
    id: 'highlight-blue',
    title: '青でハイライト',
    parentId: 'highlight-selection',
    contexts: ['selection']
  });

  chrome.contextMenus.create({
    id: 'highlight-green',
    title: '緑でハイライト',
    parentId: 'highlight-selection',
    contexts: ['selection']
  });

  chrome.contextMenus.create({
    id: 'highlight-pink',
    title: 'ピンクでハイライト',
    parentId: 'highlight-selection',
    contexts: ['selection']
  });

  chrome.contextMenus.create({
    id: 'separator-1',
    type: 'separator',
    contexts: ['selection']
  });

  chrome.contextMenus.create({
    id: 'remove-highlights',
    title: 'このページのハイライトをクリア',
    contexts: ['page']
  });

  // 初期設定を保存
  if (details.reason === 'install') {
    chrome.storage.local.set({
      isEnabled: true,
      highlightColor: '#FFEB3B',
      highlights: {}
    });
  }
});

chrome.contextMenus.createcontexts パラメータで、メニューが表示される条件を指定しています。selection はテキストが選択されているとき、page はページ上のどこでも表示されます。parentId を使うことで、サブメニューの階層構造を作れます。

コンテキストメニューのクリック処理

メニューがクリックされたときの処理を実装します。

> コンテキストメニューがクリックされたときの
  処理を実装して。選択された色で
  コンテンツスクリプトにハイライト指示を送って
// コンテキストメニューのクリックイベント
chrome.contextMenus.onClicked.addListener((info, tab) => {
  const colorMap = {
    'highlight-yellow': '#FFEB3B',
    'highlight-blue': '#64B5F6',
    'highlight-green': '#81C784',
    'highlight-pink': '#F48FB1'
  };

  if (colorMap[info.menuItemId]) {
    // コンテンツスクリプトにハイライト指示を送信
    chrome.tabs.sendMessage(tab.id, {
      action: 'highlightSelection',
      color: colorMap[info.menuItemId],
      text: info.selectionText
    });
  }

  if (info.menuItemId === 'remove-highlights') {
    chrome.tabs.sendMessage(tab.id, {
      action: 'clearAll'
    });
  }
});

info オブジェクトには、クリックされたメニューの menuItemId、選択されたテキスト selectionText、ページの URL pageUrl などの情報が含まれています。tab オブジェクトからはタブの ID を取得して、そのタブで動作しているコンテンツスクリプトにメッセージを送信します。

タブ変更とナビゲーションの監視

ユーザーがタブを切り替えたりページを移動したりしたときに、拡張機能のアイコンのバッジ(数字の表示)を更新する機能を実装します。

> タブのアクティブ変更とページナビゲーションを
  監視して、拡張アイコンにハイライト件数の
  バッジを表示する機能を実装して
// タブのアクティブ変更を監視
chrome.tabs.onActivated.addListener(async (activeInfo) => {
  await updateBadge(activeInfo.tabId);
});

// ページナビゲーションの完了を監視
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
  if (changeInfo.status === 'complete') {
    await updateBadge(tabId);
  }
});

// バッジを更新する関数
async function updateBadge(tabId) {
  try {
    const tab = await chrome.tabs.get(tabId);
    const url = tab.url;

    if (!url || url.startsWith('chrome://') || url.startsWith('chrome-extension://')) {
      chrome.action.setBadgeText({ text: '', tabId });
      return;
    }

    const result = await chrome.storage.local.get(['highlights']);
    const highlights = result.highlights || {};
    const count = (highlights[url] || []).length;

    if (count > 0) {
      chrome.action.setBadgeText({ text: String(count), tabId });
      chrome.action.setBadgeBackgroundColor({ color: '#FFEB3B', tabId });
      chrome.action.setBadgeTextColor({ color: '#333333', tabId });
    } else {
      chrome.action.setBadgeText({ text: '', tabId });
    }
  } catch (e) {
    console.error('Badge update failed:', e);
  }
}

chrome.action.setBadgeText で拡張アイコンの右下に小さな数字を表示できます。tabId を指定することで、タブごとに異なるバッジを表示できます。chrome:// で始まるシステムページではバッジを表示しないようにしています。

アラームによる定期処理

Service Worker は長時間の処理ができないため、定期的に実行したい処理には chrome.alarms API を使います。Web Highlighter では、古いハイライトデータの自動クリーンアップに使います。

> chrome.alarmsを使って、24時間ごとに
  30日以上前のハイライトデータを
  自動削除するクリーンアップ処理を実装して
// アラームの設定(インストール時)
chrome.runtime.onInstalled.addListener(() => {
  // 24時間ごとにクリーンアップ
  chrome.alarms.create('cleanup-old-highlights', {
    periodInMinutes: 60 * 24  // 24時間
  });
});

// アラームイベントのリスナー
chrome.alarms.onAlarm.addListener(async (alarm) => {
  if (alarm.name === 'cleanup-old-highlights') {
    await cleanupOldHighlights();
  }
});

async function cleanupOldHighlights() {
  const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000);
  const result = await chrome.storage.local.get(['highlights']);
  const highlights = result.highlights || {};
  let cleanedCount = 0;

  for (const url in highlights) {
    const original = highlights[url].length;
    highlights[url] = highlights[url].filter(h => h.timestamp > thirtyDaysAgo);
    cleanedCount += original - highlights[url].length;

    if (highlights[url].length === 0) {
      delete highlights[url];
    }
  }

  await chrome.storage.local.set({ highlights });
  console.log(`クリーンアップ完了: ${cleanedCount}件の古いハイライトを削除`);
}

chrome.alarms.create で名前付きアラームを作成し、chrome.alarms.onAlarm でアラーム発火時の処理を登録します。periodInMinutes を指定すると定期的に繰り返し実行されます。

演習問題

  1. コンテキストメニュー: background.js を完成させ、テキストを選択した状態で右クリックしたときに「テキストをハイライト」メニューが表示されることを確認してください。
  2. バッジ表示: ハイライトを追加した後、拡張アイコンにバッジ(件数)が表示されることを確認してください。タブを切り替えたときにバッジが更新されることも確認しましょう。
  3. 通知機能: Claude Code に「chrome.notifications APIを使って、ハイライト保存時に通知を表示する機能を追加して」と依頼してみましょう。manifest.json の permissions に notifications を追加する必要があります。
  4. キーボードショートカット: Claude Code に「manifest.jsonにキーボードショートカットを追加して、Ctrl+Shift+Hでハイライト機能のON/OFFを切り替えられるようにして」と依頼してみましょう。

参考リンク

Lecture 7メッセージ通信 — コンポーネント間でデータを送る

12:00

メッセージ通信 — コンポーネント間でデータを送る

Chrome拡張機能のポップアップ、コンテンツスクリプト、Service Worker はそれぞれ異なる実行環境で動作しています。これらのコンポーネントが連携するためには、Chrome が提供するメッセージ通信の仕組みを使う必要があります。この講義では、Web Highlighter の各コンポーネント間のメッセージ通信を Claude Code で設計・実装し、拡張機能全体を有機的に連携させていきましょう。

メッセージ通信の種類

Chrome拡張機能のメッセージ通信には、大きく分けて2つの方式があります。

ワンタイムメッセージ(One-time Message): 1回のリクエストに対して1回のレスポンスを返す方式です。chrome.runtime.sendMessagechrome.tabs.sendMessage を使います。ほとんどのユースケースではこの方式で十分です。

長期接続(Long-lived Connection): chrome.runtime.connect を使って持続的な通信チャネルを開く方式です。リアルタイムで複数のメッセージをやり取りする必要がある場合に使います。ストリーミングデータの受信や、チャットのような双方向通信に適しています。

Web Highlighter では主にワンタイムメッセージを使います。ポップアップのボタンクリックに応じてコンテンツスクリプトに指示を送り、結果を受け取るという典型的なパターンです。

通信の方向によって使う API が異なります。

送信元 送信先 API
ポップアップ → コンテンツスクリプト 特定タブの CS chrome.tabs.sendMessage(tabId, msg)
コンテンツスクリプト → Service Worker バックグラウンド chrome.runtime.sendMessage(msg)
Service Worker → コンテンツスクリプト 特定タブの CS chrome.tabs.sendMessage(tabId, msg)
ポップアップ → Service Worker バックグラウンド chrome.runtime.sendMessage(msg)

ワンタイムメッセージの実装

Claude Code にメッセージ通信の基盤を実装してもらいましょう。

> Web Highlighterの各コンポーネント間の
  メッセージ通信を設計・実装して。
  メッセージの型を定義して、
  送信側と受信側のコードを書いて。
  以下のメッセージを実装して:
  - toggleHighlight: ハイライトON/OFF
  - highlightSelection: テキストをハイライト
  - getCount: ハイライト件数取得
  - clearAll: 全ハイライト削除
  - getHighlightList: ハイライト一覧取得

Claude Code が生成するメッセージ通信の設計です。まず、メッセージの型を定義します。

// messages.js - メッセージ型の定義(共有リファレンス)
const MessageTypes = {
  TOGGLE_HIGHLIGHT: 'toggleHighlight',
  HIGHLIGHT_SELECTION: 'highlightSelection',
  GET_COUNT: 'getCount',
  CLEAR_ALL: 'clearAll',
  GET_HIGHLIGHT_LIST: 'getHighlightList',
  REMOVE_HIGHLIGHT: 'removeHighlight',
  UPDATE_SETTINGS: 'updateSettings',
  HIGHLIGHT_ADDED: 'highlightAdded',
  HIGHLIGHT_REMOVED: 'highlightRemoved'
};

次に、ポップアップからコンテンツスクリプトへの送信処理です。

// popup.js - ポップアップからの送信

// アクティブタブのコンテンツスクリプトにメッセージを送る関数
async function sendToActiveTab(message) {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  if (!tab || !tab.id) {
    console.warn('アクティブタブが見つかりません');
    return null;
  }

  try {
    const response = await chrome.tabs.sendMessage(tab.id, message);
    return response;
  } catch (error) {
    console.error('メッセージ送信エラー:', error);
    return null;
  }
}

// 使用例: ハイライト件数を取得
async function loadHighlightCount() {
  const response = await sendToActiveTab({
    action: MessageTypes.GET_COUNT
  });

  if (response) {
    document.getElementById('highlight-count').textContent = response.count;
  }
}

// 使用例: ハイライト一覧を取得
async function loadHighlightList() {
  const response = await sendToActiveTab({
    action: MessageTypes.GET_HIGHLIGHT_LIST
  });

  if (response && response.highlights) {
    renderHighlightList(response.highlights);
  }
}

function renderHighlightList(highlights) {
  const listContainer = document.getElementById('highlight-list');
  listContainer.innerHTML = '';

  highlights.forEach(h => {
    const item = document.createElement('div');
    item.className = 'highlight-item';
    item.innerHTML = `
      <span class="highlight-text" style="border-left: 3px solid ${h.color}; padding-left: 8px;">
        ${h.text.substring(0, 50)}${h.text.length > 50 ? '...' : ''}
      </span>
      <button class="remove-btn" data-id="${h.id}">削除</button>
    `;
    listContainer.appendChild(item);
  });
}

コンテンツスクリプト側の受信処理

コンテンツスクリプトでメッセージを受信し、適切な処理を実行するリスナーを実装します。

> content.jsのメッセージリスナーを整理して。
  すべてのメッセージ型に対応するswitch文を作り、
  非同期レスポンスにも対応して
// content.js - メッセージ受信リスナー

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  // 非同期処理に対応するためtrueを返す
  handleMessage(message).then(sendResponse);
  return true;
});

async function handleMessage(message) {
  switch (message.action) {
    case 'toggleHighlight':
      isEnabled = message.enabled;
      return { success: true, enabled: isEnabled };

    case 'highlightSelection':
      const selection = window.getSelection();
      if (selection && !selection.isCollapsed) {
        const prevColor = highlightColor;
        highlightColor = message.color || highlightColor;
        applyHighlight(selection);
        highlightColor = prevColor;
        return { success: true };
      }
      return { success: false, error: 'テキストが選択されていません' };

    case 'getCount':
      return { count: highlights.length };

    case 'clearAll':
      clearAllHighlights();
      return { success: true };

    case 'getHighlightList':
      return {
        highlights: highlights.map(h => ({
          id: h.id,
          text: h.text,
          color: h.color,
          timestamp: h.timestamp
        }))
      };

    case 'removeHighlight':
      removeHighlight(message.highlightId);
      return { success: true };

    case 'updateSettings':
      if (message.settings) {
        applySettings(message.settings);
      }
      return { success: true };

    default:
      return { error: `未知のアクション: ${message.action}` };
  }
}

function clearAllHighlights() {
  document.querySelectorAll('.web-highlighter-mark').forEach(span => {
    const parent = span.parentNode;
    while (span.firstChild) {
      parent.insertBefore(span.firstChild, span);
    }
    parent.removeChild(span);
    parent.normalize();
  });
  highlights = [];
  saveHighlights();
}

重要なポイントは return true です。chrome.runtime.onMessage.addListener のコールバックで true を返すと、sendResponse を非同期的に呼び出せるようになります。これを忘れると、非同期処理が完了する前にメッセージチャネルが閉じてしまい、レスポンスが送信元に届きません。

Service Worker でのメッセージ中継

バックグラウンドの Service Worker がメッセージの中継役を果たす場合があります。たとえば、あるタブのコンテンツスクリプトから別のタブのコンテンツスクリプトにメッセージを送りたい場合です。

> Service Workerでメッセージを受信して、
  必要に応じて他のタブに転送する
  中継処理を実装して
// background.js - メッセージ中継

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  handleBackgroundMessage(message, sender).then(sendResponse);
  return true;
});

async function handleBackgroundMessage(message, sender) {
  switch (message.action) {
    case 'highlightAdded': {
      // コンテンツスクリプトからの通知をバッジに反映
      if (sender.tab) {
        await updateBadge(sender.tab.id);
      }
      return { received: true };
    }

    case 'highlightRemoved': {
      if (sender.tab) {
        await updateBadge(sender.tab.id);
      }
      return { received: true };
    }

    case 'broadcastToAllTabs': {
      // すべてのタブにメッセージをブロードキャスト
      const tabs = await chrome.tabs.query({});
      const results = await Promise.allSettled(
        tabs.map(tab =>
          chrome.tabs.sendMessage(tab.id, message.payload).catch(() => null)
        )
      );
      return {
        sent: results.filter(r => r.status === 'fulfilled').length,
        total: tabs.length
      };
    }

    case 'getTabInfo': {
      const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
      return {
        url: tab?.url,
        title: tab?.title,
        id: tab?.id
      };
    }

    default:
      return { error: `未知のアクション: ${message.action}` };
  }
}

Promise.allSettled を使って全タブにメッセージを送信しています。一部のタブでコンテンツスクリプトが読み込まれていない場合(chrome:// ページなど)にエラーが発生しますが、allSettled なら他のタブへの送信は継続されます。

長期接続(Port)の実装

リアルタイムで頻繁にデータをやり取りする必要がある場合は、Port による長期接続を使います。

> chrome.runtime.connectを使った
  長期接続の実装例を見せて。
  ポップアップが開いている間、
  リアルタイムでハイライト件数を
  更新する仕組みを作って
// popup.js - 長期接続の送信側
let port = null;

function connectToBackground() {
  port = chrome.runtime.connect({ name: 'popup-connection' });

  port.onMessage.addListener((msg) => {
    if (msg.type === 'countUpdate') {
      document.getElementById('highlight-count').textContent = msg.count;
    }
    if (msg.type === 'highlightEvent') {
      console.log(`ハイライトイベント: ${msg.event}`, msg.data);
    }
  });

  port.onDisconnect.addListener(() => {
    console.log('ポートが切断されました');
    port = null;
  });

  // 初期データをリクエスト
  port.postMessage({ type: 'init' });
}

// ポップアップ表示時に接続
document.addEventListener('DOMContentLoaded', connectToBackground);
// background.js - 長期接続の受信側
const connectedPorts = new Set();

chrome.runtime.onConnect.addListener((port) => {
  if (port.name === 'popup-connection') {
    connectedPorts.add(port);

    port.onMessage.addListener(async (msg) => {
      if (msg.type === 'init') {
        const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
        if (tab) {
          const result = await chrome.storage.local.get(['highlights']);
          const highlights = result.highlights || {};
          const count = (highlights[tab.url] || []).length;
          port.postMessage({ type: 'countUpdate', count });
        }
      }
    });

    port.onDisconnect.addListener(() => {
      connectedPorts.delete(port);
    });
  }
});

// ハイライト変更時に接続中のポートに通知
function notifyPorts(event, data) {
  connectedPorts.forEach(port => {
    try {
      port.postMessage({ type: 'highlightEvent', event, data });
    } catch (e) {
      connectedPorts.delete(port);
    }
  });
}

長期接続は強力ですが、ポップアップが閉じるとポートも切断されます。ポップアップの短い寿命を考慮すると、ワンタイムメッセージで十分なケースがほとんどです。

エラーハンドリングのベストプラクティス

メッセージ通信でよくあるエラーと対処法を整理しましょう。

// エラーハンドリング付きの安全なメッセージ送信
async function safeSendMessage(tabId, message, timeout = 5000) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      reject(new Error('メッセージ送信タイムアウト'));
    }, timeout);

    chrome.tabs.sendMessage(tabId, message, (response) => {
      clearTimeout(timer);

      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message));
        return;
      }

      resolve(response);
    });
  });
}

// 使用例
async function safeGetCount(tabId) {
  try {
    const response = await safeSendMessage(tabId, { action: 'getCount' });
    return response.count;
  } catch (error) {
    if (error.message.includes('Receiving end does not exist')) {
      console.warn('コンテンツスクリプトがまだ読み込まれていません');
      return 0;
    }
    console.error('件数取得エラー:', error);
    return 0;
  }
}

chrome.runtime.lastError は Chrome拡張機能特有のエラーハンドリング機構です。メッセージの送信先が存在しない場合(コンテンツスクリプトが読み込まれていないページなど)にエラーが設定されます。

演習問題

  1. メッセージ送信: ポップアップから「ハイライト一覧取得」メッセージを送り、コンテンツスクリプトからハイライトのリストを受け取って表示する機能を実装してください。
  2. エラー確認: chrome://extensions ページでポップアップを開き、コンテンツスクリプトが存在しないページへのメッセージ送信でどのようなエラーが出るか確認してください。
  3. ブロードキャスト: Claude Code に「すべてのタブのコンテンツスクリプトに設定変更を通知するブロードキャスト機能を作って」と依頼してみましょう。
  4. デバッグ: Service Worker のコンソールログを確認する方法を試してください。chrome://extensions の「Service Worker」リンクをクリックすると DevTools が開きます。

参考リンク

Lecture 8外部API連携 — Webサービスと接続する

12:00

外部API連携 — Webサービスと接続する

Chrome拡張機能は、外部の Web API と通信して機能を拡張できます。Web Highlighter に翻訳機能やブックマーク同期を追加するなど、API 連携は拡張機能の可能性を大きく広げます。しかし、拡張機能からの API 呼び出しには通常の Web アプリとは異なるルールがあります。この講義では、CORS の扱い、host_permissions の設定、fetch API の使い方、そして実際の API 連携の実装を Claude Code で行っていきましょう。

拡張機能における CORS の扱い

通常の Web ページでは、異なるオリジン(ドメイン)への HTTP リクエストは CORS(Cross-Origin Resource Sharing)ポリシーによって制限されます。しかし、Chrome拡張機能では状況が異なります。

Service Worker からのリクエスト: バックグラウンドの Service Worker から発行されるリクエストは、host_permissions に指定されたオリジンに対して CORS の制限を受けません。これは拡張機能の大きな利点です。

コンテンツスクリプトからのリクエスト: コンテンツスクリプトから発行されるリクエストは、そのページのオリジンから発行されたものとして扱われます。つまり、通常の Web ページと同じ CORS 制限を受けます。API 呼び出しはコンテンツスクリプトから直接行わず、Service Worker に中継してもらうのがベストプラクティスです。

ポップアップからのリクエスト: ポップアップは拡張機能のオリジン(chrome-extension://...)で動作するため、host_permissions に記載されたドメインへのリクエストは CORS 制限を受けません。

manifest.json に必要な host_permissions を追加しましょう。

> manifest.jsonのhost_permissionsに、
  翻訳APIとブックマークAPIのドメインを
  追加する方法を教えて
{
  "host_permissions": [
    "<all_urls>",
    "https://api.mymemory.translated.net/*",
    "https://api.example.com/*"
  ]
}

<all_urls> をすでに指定している場合はすべてのドメインへのアクセスが許可されていますが、本番環境では必要なドメインだけを個別に指定することが推奨されます。権限が少ないほど、Chrome Web Store の審査で有利になります。

fetch API で外部 API を呼び出す

Chrome拡張機能での API 呼び出しは、標準の fetch API を使います。Claude Code に翻訳 API の連携を実装してもらいましょう。

> Web Highlighterに翻訳機能を追加して。
  ハイライトしたテキストを無料の翻訳API
  (MyMemory Translation API)を使って
  英語に翻訳する機能を実装して。
  Service Workerで翻訳リクエストを処理して

Claude Code が生成する Service Worker 側の翻訳処理です。

// background.js - 翻訳API連携

async function translateText(text, sourceLang = 'ja', targetLang = 'en') {
  const endpoint = 'https://api.mymemory.translated.net/get';
  const params = new URLSearchParams({
    q: text,
    langpair: `${sourceLang}|${targetLang}`
  });

  try {
    const response = await fetch(`${endpoint}?${params}`);

    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }

    const data = await response.json();

    if (data.responseStatus === 200) {
      return {
        success: true,
        original: text,
        translated: data.responseData.translatedText,
        sourceLang,
        targetLang
      };
    } else {
      throw new Error(`Translation error: ${data.responseStatus}`);
    }
  } catch (error) {
    console.error('翻訳エラー:', error);
    return {
      success: false,
      error: error.message
    };
  }
}

// メッセージリスナーに翻訳処理を追加
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action === 'translate') {
    translateText(message.text, message.sourceLang, message.targetLang)
      .then(sendResponse);
    return true;  // 非同期レスポンスのために必要
  }
});

コンテンツスクリプトから翻訳をリクエストする側のコードです。

// content.js - 翻訳リクエスト

async function translateHighlight(highlightId) {
  const highlight = highlights.find(h => h.id === highlightId);
  if (!highlight) return;

  // Service Worker に翻訳を依頼
  const result = await chrome.runtime.sendMessage({
    action: 'translate',
    text: highlight.text,
    sourceLang: 'ja',
    targetLang: 'en'
  });

  if (result.success) {
    showTranslationTooltip(highlightId, result.translated);
  } else {
    console.error('翻訳失敗:', result.error);
  }
}

function showTranslationTooltip(highlightId, translatedText) {
  const span = document.querySelector(`[data-highlight-id="${highlightId}"]`);
  if (!span) return;

  // 既存のツールチップを削除
  const existing = document.querySelector('.wh-tooltip');
  if (existing) existing.remove();

  const tooltip = document.createElement('div');
  tooltip.className = 'wh-tooltip';
  tooltip.textContent = translatedText;
  tooltip.style.cssText = `
    position: absolute;
    background: #333;
    color: white;
    padding: 8px 12px;
    border-radius: 6px;
    font-size: 14px;
    max-width: 300px;
    z-index: 999999;
    box-shadow: 0 2px 8px rgba(0,0,0,0.3);
  `;

  const rect = span.getBoundingClientRect();
  tooltip.style.left = `${rect.left + window.scrollX}px`;
  tooltip.style.top = `${rect.bottom + window.scrollY + 8}px`;

  document.body.appendChild(tooltip);

  // 3秒後に自動で消す
  setTimeout(() => tooltip.remove(), 3000);
}

API キーの安全な管理

多くの API では認証にAPIキーが必要です。Chrome拡張機能での API キーの管理にはいくつかの方法があります。

> Chrome拡張機能でAPIキーを安全に
  管理する方法を教えて。
  ベストプラクティスを示して

ハードコードは禁止: ソースコードに API キーを直接書くのは絶対に避けてください。Chrome拡張機能のソースコードはユーザーが閲覧可能です。

chrome.storage に保存: ユーザーがオプションページで入力した API キーを chrome.storage.local に保存する方法です。

// options.js - APIキーの保存
document.getElementById('save-api-key').addEventListener('click', () => {
  const apiKey = document.getElementById('api-key-input').value;
  chrome.storage.local.set({ apiKey }, () => {
    showStatus('APIキーを保存しました');
  });
});

// background.js - APIキーの使用
async function callApiWithKey(endpoint, params) {
  const result = await chrome.storage.local.get(['apiKey']);
  if (!result.apiKey) {
    throw new Error('APIキーが設定されていません。設定ページで入力してください。');
  }

  const response = await fetch(endpoint, {
    headers: {
      'Authorization': `Bearer ${result.apiKey}`,
      'Content-Type': 'application/json'
    },
    ...params
  });

  return response.json();
}

バックエンドプロキシ: 本格的な拡張機能では、自前のバックエンドサーバーを経由して API を呼び出します。API キーはサーバー側で管理し、拡張機能はサーバーのエンドポイントにリクエストを送ります。

レート制限とエラーハンドリング

外部 API を呼び出す際は、レート制限(Rate Limiting)への対応が不可欠です。

> API呼び出しにレート制限とリトライ処理を
  追加して。指数バックオフで最大3回
  リトライする実装にして
// api-utils.js - API ユーティリティ

class ApiClient {
  constructor(maxRetries = 3, baseDelay = 1000) {
    this.maxRetries = maxRetries;
    this.baseDelay = baseDelay;
    this.requestQueue = [];
    this.isProcessing = false;
  }

  async fetchWithRetry(url, options = {}) {
    let lastError;

    for (let attempt = 0; attempt < this.maxRetries; attempt++) {
      try {
        const response = await fetch(url, options);

        if (response.status === 429) {
          // レート制限 - 指数バックオフで待機
          const delay = this.baseDelay * Math.pow(2, attempt);
          console.warn(`レート制限。${delay}ms 後にリトライ (${attempt + 1}/${this.maxRetries})`);
          await this.sleep(delay);
          continue;
        }

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        return await response.json();
      } catch (error) {
        lastError = error;

        if (attempt < this.maxRetries - 1) {
          const delay = this.baseDelay * Math.pow(2, attempt);
          console.warn(`リクエスト失敗。${delay}ms 後にリトライ:`, error.message);
          await this.sleep(delay);
        }
      }
    }

    throw new Error(`${this.maxRetries}回のリトライ後も失敗: ${lastError.message}`);
  }

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  // リクエストキューによるレート制限
  async enqueue(requestFn) {
    return new Promise((resolve, reject) => {
      this.requestQueue.push({ requestFn, resolve, reject });
      this.processQueue();
    });
  }

  async processQueue() {
    if (this.isProcessing || this.requestQueue.length === 0) return;

    this.isProcessing = true;
    const { requestFn, resolve, reject } = this.requestQueue.shift();

    try {
      const result = await requestFn();
      resolve(result);
    } catch (error) {
      reject(error);
    }

    // 最低100msの間隔を空ける
    await this.sleep(100);
    this.isProcessing = false;
    this.processQueue();
  }
}

const apiClient = new ApiClient();

指数バックオフ(Exponential Backoff)は、リトライのたびに待機時間を2倍にしていく戦略です。1回目は 1000ms、2回目は 2000ms、3回目は 4000ms と増えていきます。これにより、サーバーへの過負荷を防ぎつつ、一時的な障害からの回復を試みることができます。

演習問題

  1. 翻訳機能: Claude Code を使って翻訳機能を実装し、ハイライトしたテキストが英語に翻訳されることを確認してください。
  2. 別の API: 無料の辞書 API(例: Free Dictionary API — https://dictionaryapi.dev/)を使って、ハイライトした英単語の定義を表示する機能を追加してみましょう。
  3. エラーテスト: ネットワークを一時的にオフにした状態で翻訳機能を使い、エラーハンドリングが正しく動作するか確認してください。
  4. キャッシュ実装: Claude Code に「同じテキストの翻訳結果をchrome.storageにキャッシュして、2回目以降はAPIを呼ばないようにして」と依頼してみましょう。

参考リンク

Lecture 9オプションページ — 設定画面を作る

12:00

オプションページ — 設定画面を作る

オプションページは、拡張機能のユーザー設定を管理するための専用画面です。Web Highlighter では、デフォルトのハイライト色、キーボードショートカットの設定、ハイライトデータのエクスポート・インポート機能をオプションページに実装します。Claude Code を使って、実用的で使いやすい設定画面を構築していきましょう。

オプションページの種類

Chrome拡張機能のオプションページには2つの表示方式があります。

フルページ方式: options_page フィールドで指定します。独立したタブとして開かれ、大きな画面スペースを使えます。複雑な設定画面や多くの項目がある場合に適しています。

{
  "options_page": "options/options.html"
}

埋め込み方式: options_ui フィールドで指定します。chrome://extensions ページ内にインラインフレームとして表示されます。シンプルな設定にはこちらが適しています。

{
  "options_ui": {
    "page": "options/options.html",
    "open_in_tab": false
  }
}

Web Highlighter では、エクスポート・インポート機能など比較的多くの項目があるため、フルページ方式を採用します。open_in_tab: true を指定するか、options_page を使います。

オプションページはポップアップから chrome.runtime.openOptionsPage() で開くことができます。また、chrome://extensions の拡張機能カードにある「オプション」リンクからもアクセスできます。

options.html を作成する

Claude Code にオプションページの HTML を生成してもらいましょう。

> Web Highlighterのオプションページを作って。
  以下のセクションを含めて:
  1. デフォルトハイライト色のカラーピッカー
  2. ハイライトの透明度(opacity)スライダー
  3. キーボードショートカットの設定
  4. ハイライトデータのエクスポート(JSON)
  5. ハイライトデータのインポート
  6. 全データの削除ボタン
  モダンで見やすいデザインにして

Claude Code が生成する options.html です。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Web Highlighter - 設定</title>
  <link rel="stylesheet" href="options.css">
</head>
<body>
  <div class="container">
    <header>
      <h1>Web Highlighter 設定</h1>
      <p class="subtitle">ハイライト機能のカスタマイズ</p>
    </header>

    <section class="setting-group">
      <h2>外観設定</h2>

      <div class="setting-item">
        <label for="default-color">デフォルトハイライト色</label>
        <div class="color-picker-wrapper">
          <input type="color" id="default-color" value="#FFEB3B">
          <span id="color-hex" class="color-hex">#FFEB3B</span>
        </div>
      </div>

      <div class="setting-item">
        <label for="preset-colors">プリセットカラー</label>
        <div class="preset-colors" id="preset-colors">
          <button class="preset-btn" data-color="#FFEB3B" style="background:#FFEB3B" title="黄色"></button>
          <button class="preset-btn" data-color="#64B5F6" style="background:#64B5F6" title="青"></button>
          <button class="preset-btn" data-color="#81C784" style="background:#81C784" title="緑"></button>
          <button class="preset-btn" data-color="#F48FB1" style="background:#F48FB1" title="ピンク"></button>
          <button class="preset-btn" data-color="#FFB74D" style="background:#FFB74D" title="オレンジ"></button>
          <button class="preset-btn" data-color="#CE93D8" style="background:#CE93D8" title="紫"></button>
        </div>
      </div>

      <div class="setting-item">
        <label for="opacity">ハイライトの透明度: <span id="opacity-value">80</span>%</label>
        <input type="range" id="opacity" min="20" max="100" value="80" class="slider">
      </div>
    </section>

    <section class="setting-group">
      <h2>動作設定</h2>

      <div class="setting-item">
        <label>
          <input type="checkbox" id="auto-highlight" checked>
          テキスト選択時に自動でハイライト
        </label>
      </div>

      <div class="setting-item">
        <label>
          <input type="checkbox" id="show-tooltip" checked>
          ハイライトにツールチップを表示
        </label>
      </div>

      <div class="setting-item">
        <label>
          <input type="checkbox" id="restore-on-load" checked>
          ページ読み込み時にハイライトを復元
        </label>
      </div>
    </section>

    <section class="setting-group">
      <h2>データ管理</h2>

      <div class="setting-item data-actions">
        <button id="export-btn" class="btn btn-primary">
          データをエクスポート(JSON)
        </button>
        <div class="import-wrapper">
          <button id="import-btn" class="btn btn-secondary">
            データをインポート
          </button>
          <input type="file" id="import-file" accept=".json" hidden>
        </div>
      </div>

      <div class="setting-item stats">
        <div class="stat">
          <span class="stat-label">保存サイト数</span>
          <span id="site-count" class="stat-value">0</span>
        </div>
        <div class="stat">
          <span class="stat-label">総ハイライト数</span>
          <span id="total-count" class="stat-value">0</span>
        </div>
        <div class="stat">
          <span class="stat-label">ストレージ使用量</span>
          <span id="storage-usage" class="stat-value">0 KB</span>
        </div>
      </div>

      <div class="setting-item danger-zone">
        <h3>危険な操作</h3>
        <button id="clear-all-btn" class="btn btn-danger">
          すべてのハイライトデータを削除
        </button>
      </div>
    </section>

    <div id="status-message" class="status-message hidden"></div>
  </div>

  <script src="options.js"></script>
</body>
</html>

options.css でスタイリングする

> options.cssを作って。カード形式のセクション、
  スムーズなアニメーション、レスポンシブ対応で。
  ブラウザ拡張の設定画面として洗練されたデザインに
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background-color: #f0f2f5;
  color: #333;
  line-height: 1.6;
}

.container {
  max-width: 640px;
  margin: 0 auto;
  padding: 32px 16px;
}

header {
  margin-bottom: 32px;
}

header h1 {
  font-size: 24px;
  font-weight: 700;
  color: #1a1a1a;
}

.subtitle {
  color: #666;
  font-size: 14px;
  margin-top: 4px;
}

.setting-group {
  background: white;
  border-radius: 12px;
  padding: 24px;
  margin-bottom: 20px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}

.setting-group h2 {
  font-size: 16px;
  font-weight: 600;
  color: #1a1a1a;
  margin-bottom: 20px;
  padding-bottom: 12px;
  border-bottom: 1px solid #e8e8e8;
}

.setting-item {
  margin-bottom: 20px;
}

.setting-item:last-child {
  margin-bottom: 0;
}

.setting-item label {
  display: block;
  font-size: 14px;
  font-weight: 500;
  color: #444;
  margin-bottom: 8px;
}

.color-picker-wrapper {
  display: flex;
  align-items: center;
  gap: 12px;
}

input[type="color"] {
  width: 48px;
  height: 48px;
  border: 2px solid #ddd;
  border-radius: 8px;
  cursor: pointer;
  padding: 2px;
}

.color-hex {
  font-family: 'Courier New', monospace;
  font-size: 14px;
  color: #666;
}

.preset-colors {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.preset-btn {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  border: 2px solid transparent;
  cursor: pointer;
  transition: all 0.2s;
}

.preset-btn:hover {
  transform: scale(1.15);
  border-color: #333;
}

.slider {
  width: 100%;
  height: 6px;
  -webkit-appearance: none;
  appearance: none;
  background: #e0e0e0;
  border-radius: 3px;
  outline: none;
}

.slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 20px;
  height: 20px;
  background: #4CAF50;
  border-radius: 50%;
  cursor: pointer;
}

.btn {
  padding: 10px 20px;
  border: none;
  border-radius: 8px;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: background-color 0.2s;
}

.btn-primary {
  background-color: #1976D2;
  color: white;
}

.btn-primary:hover { background-color: #1565C0; }

.btn-secondary {
  background-color: #e0e0e0;
  color: #333;
}

.btn-secondary:hover { background-color: #bdbdbd; }

.btn-danger {
  background-color: #ef5350;
  color: white;
}

.btn-danger:hover { background-color: #d32f2f; }

.data-actions {
  display: flex;
  gap: 12px;
}

.stats {
  display: flex;
  gap: 16px;
  background: #f8f9fa;
  border-radius: 8px;
  padding: 16px;
}

.stat {
  flex: 1;
  text-align: center;
}

.stat-label {
  display: block;
  font-size: 11px;
  color: #888;
  margin-bottom: 4px;
}

.stat-value {
  display: block;
  font-size: 20px;
  font-weight: 700;
  color: #1a1a1a;
}

.danger-zone {
  border-top: 1px solid #ffcdd2;
  padding-top: 16px;
  margin-top: 20px;
}

.danger-zone h3 {
  font-size: 14px;
  color: #d32f2f;
  margin-bottom: 12px;
}

.status-message {
  position: fixed;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%);
  padding: 12px 24px;
  border-radius: 8px;
  background: #333;
  color: white;
  font-size: 14px;
  transition: opacity 0.3s;
}

.status-message.hidden {
  opacity: 0;
  pointer-events: none;
}

options.js で設定ロジックを実装する

設定の保存・読み込み、エクスポート・インポートのロジックを実装します。

> options.jsを書いて。設定の保存と読み込み、
  エクスポート・インポート機能を実装して。
  設定変更時にリアルタイムでchrome.storageに
  保存して、ステータスメッセージを表示して
// options.js

document.addEventListener('DOMContentLoaded', async () => {
  // DOM要素の取得
  const defaultColor = document.getElementById('default-color');
  const colorHex = document.getElementById('color-hex');
  const opacity = document.getElementById('opacity');
  const opacityValue = document.getElementById('opacity-value');
  const autoHighlight = document.getElementById('auto-highlight');
  const showTooltip = document.getElementById('show-tooltip');
  const restoreOnLoad = document.getElementById('restore-on-load');
  const exportBtn = document.getElementById('export-btn');
  const importBtn = document.getElementById('import-btn');
  const importFile = document.getElementById('import-file');
  const clearAllBtn = document.getElementById('clear-all-btn');

  // 保存された設定を読み込む
  const result = await chrome.storage.sync.get({
    defaultColor: '#FFEB3B',
    opacity: 80,
    autoHighlight: true,
    showTooltip: true,
    restoreOnLoad: true
  });

  defaultColor.value = result.defaultColor;
  colorHex.textContent = result.defaultColor;
  opacity.value = result.opacity;
  opacityValue.textContent = result.opacity;
  autoHighlight.checked = result.autoHighlight;
  showTooltip.checked = result.showTooltip;
  restoreOnLoad.checked = result.restoreOnLoad;

  // カラーピッカーの変更
  defaultColor.addEventListener('input', (e) => {
    const color = e.target.value;
    colorHex.textContent = color;
    saveSettings({ defaultColor: color });
  });

  // プリセットカラーのクリック
  document.querySelectorAll('.preset-btn').forEach(btn => {
    btn.addEventListener('click', () => {
      const color = btn.dataset.color;
      defaultColor.value = color;
      colorHex.textContent = color;
      saveSettings({ defaultColor: color });
    });
  });

  // 透明度の変更
  opacity.addEventListener('input', (e) => {
    opacityValue.textContent = e.target.value;
    saveSettings({ opacity: parseInt(e.target.value) });
  });

  // チェックボックスの変更
  autoHighlight.addEventListener('change', () => {
    saveSettings({ autoHighlight: autoHighlight.checked });
  });

  showTooltip.addEventListener('change', () => {
    saveSettings({ showTooltip: showTooltip.checked });
  });

  restoreOnLoad.addEventListener('change', () => {
    saveSettings({ restoreOnLoad: restoreOnLoad.checked });
  });

  // エクスポート
  exportBtn.addEventListener('click', async () => {
    const data = await chrome.storage.local.get(['highlights']);
    const blob = new Blob(
      [JSON.stringify(data.highlights || {}, null, 2)],
      { type: 'application/json' }
    );
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `web-highlighter-backup-${formatDate()}.json`;
    a.click();
    URL.revokeObjectURL(url);
    showStatus('データをエクスポートしました');
  });

  // インポート
  importBtn.addEventListener('click', () => importFile.click());

  importFile.addEventListener('change', (e) => {
    const file = e.target.files[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = async (event) => {
      try {
        const highlights = JSON.parse(event.target.result);
        await chrome.storage.local.set({ highlights });
        showStatus('データをインポートしました');
        updateStats();
      } catch (error) {
        showStatus('インポート失敗: 無効なJSONファイルです', true);
      }
    };
    reader.readAsText(file);
  });

  // 全削除
  clearAllBtn.addEventListener('click', async () => {
    if (confirm('すべてのハイライトデータを削除しますか?この操作は取り消せません。')) {
      await chrome.storage.local.set({ highlights: {} });
      showStatus('すべてのデータを削除しました');
      updateStats();
    }
  });

  // 統計情報を更新
  updateStats();
});

async function saveSettings(settings) {
  await chrome.storage.sync.set(settings);
  showStatus('設定を保存しました');
}

async function updateStats() {
  const result = await chrome.storage.local.get(['highlights']);
  const highlights = result.highlights || {};
  const siteCount = Object.keys(highlights).length;
  const totalCount = Object.values(highlights).reduce(
    (sum, arr) => sum + arr.length, 0
  );

  document.getElementById('site-count').textContent = siteCount;
  document.getElementById('total-count').textContent = totalCount;

  chrome.storage.local.getBytesInUse(null, (bytes) => {
    const kb = (bytes / 1024).toFixed(1);
    document.getElementById('storage-usage').textContent = `${kb} KB`;
  });
}

function showStatus(message, isError = false) {
  const el = document.getElementById('status-message');
  el.textContent = message;
  el.style.background = isError ? '#d32f2f' : '#333';
  el.classList.remove('hidden');
  setTimeout(() => el.classList.add('hidden'), 2500);
}

function formatDate() {
  const d = new Date();
  return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
}

設定は chrome.storage.sync に保存しているため、同じ Google アカウントの他のデバイスにも自動的に同期されます。ハイライトデータ本体は容量が大きいため chrome.storage.local に保存しています。

演習問題

  1. オプションページ作成: Claude Code を使ってオプションページを完成させ、ポップアップの「設定」ボタンからオプションページが開くことを確認してください。
  2. エクスポート・インポート: いくつかのページでハイライトを追加した後、エクスポートしてみましょう。その後、データを全削除してからインポートし、ハイライトが復元されることを確認してください。
  3. カスタムテーマ: Claude Code に「オプションページにダークテーマとライトテーマの切り替え機能を追加して」と依頼してみましょう。
  4. 設定の反映: 透明度を変更した後、すでにハイライトされたテキストの透明度がリアルタイムで変わることを確認してください(chrome.storage.onChanged リスナーが必要です)。

参考リンク

Lecture 10Chrome Web Storeに公開 — 拡張機能を配布する

12:00

Chrome Web Storeに公開 — 拡張機能を配布する

Web Highlighter のすべての機能が完成しました。最後の仕上げとして、Chrome Web Store に公開して世界中のユーザーに配布しましょう。この講義では、公開前の最終チェック、パッケージング、開発者アカウントの作成、ストアリスティングの作成、審査プロセス、そして公開後のアップデート方法まで、Claude Code を活用しながら順を追って解説します。

公開前の最終チェック

Chrome Web Store に提出する前に、拡張機能の品質を確認しましょう。Claude Code にコードレビューを依頼します。

> Web Highlighter拡張のコード全体をレビューして。
  以下の観点でチェックして:
  - manifest.jsonの必須フィールドの漏れ
  - 未使用の権限がないか
  - エラーハンドリングの不足
  - セキュリティ上の問題
  - パフォーマンスの問題

権限の最小化: manifest.json の permissionshost_permissions を見直し、実際に使っている権限だけを残します。不要な権限があると、Chrome Web Store の審査で却下される可能性があります。

{
  "permissions": [
    "storage",
    "activeTab",
    "contextMenus"
  ],
  "host_permissions": [
    "<all_urls>"
  ]
}

<all_urls> は審査が厳しくなる原因になります。特定のサイトのみで動作する拡張機能であれば、具体的なパターンに置き換えましょう。Web Highlighter はすべてのサイトで動作する必要があるため、<all_urls> を維持しますが、その正当性を審査時に説明する準備が必要です。

プライバシーポリシー: ユーザーデータを扱う拡張機能には、プライバシーポリシーが必要です。Claude Code に草案を作ってもらいましょう。

> Web Highlighter拡張のプライバシーポリシーの
  草案を作って。以下の点を含めて:
  - 収集するデータの種類
  - データの保存場所
  - 第三者との共有の有無
  - データの削除方法

Web Highlighter は外部サーバーにデータを送信しない(すべてローカル保存)ため、プライバシーポリシーはシンプルになります。

拡張機能のパッケージング

Chrome Web Store に提出するには、拡張機能を ZIP ファイルにパッケージングする必要があります。

> Web Highlighter拡張をChrome Web Store用に
  パッケージングして。不要なファイルを除外して、
  ZIP形式で出力して。開発用ファイルは含めないで

パッケージに含めるファイルと含めないファイルを整理します。

含めるファイル: - manifest.json - popup/ ディレクトリ一式 - content/ ディレクトリ一式 - background/ ディレクトリ一式 - options/ ディレクトリ一式 - icons/ ディレクトリ一式

含めないファイル: - node_modules/ - .git/ - *.map ファイル(ソースマップ) - テスト用ファイル - README.md - 開発用のスクリプト

Claude Code にパッケージングスクリプトを作ってもらいます。

> パッケージング用のシェルスクリプトを作って。
  不要なファイルを除外してZIPに圧縮して。
  バージョン番号をファイル名に含めて
#!/bin/bash
# package.sh - Chrome拡張機能のパッケージング

VERSION=$(node -pe "require('./manifest.json').version")
OUTPUT="web-highlighter-v${VERSION}.zip"

# 既存のZIPを削除
rm -f "$OUTPUT"

# パッケージに含めるファイルをZIP化
zip -r "$OUTPUT" \
  manifest.json \
  popup/ \
  content/ \
  background/ \
  options/ \
  icons/ \
  -x "*.DS_Store" \
  -x "*__MACOSX*" \
  -x "*.map"

echo "パッケージ完了: $OUTPUT ($(du -h $OUTPUT | cut -f1))"

Chrome Web Store 開発者アカウントの作成

拡張機能を公開するには、Chrome Web Store の開発者アカウントが必要です。

  1. Google アカウント: 公開に使用する Google アカウントでログインします。個人用と分けたい場合は、専用のアカウントを作成することを推奨します。

  2. 開発者登録: Chrome Web Store Developer Dashboard にアクセスし、「アカウントを登録」をクリックします。

  3. 登録料: 開発者登録には1回限り5ドル(約750円)の登録料が必要です。クレジットカードで支払います。この料金は1回だけで、以降は何個でも拡張機能を公開できます。

  4. 開発者情報の入力: 表示名、メールアドレス、ウェブサイト URL を入力します。これらはストアページに表示されます。

ストアリスティングの作成

Chrome Web Store のリスティング(商品ページ)に必要な素材を準備します。

> Chrome Web Storeのリスティングに必要な
  素材の一覧と、それぞれのサイズ・形式を
  教えて。Web Highlighter用の説明文も作って

必要な素材:

素材 サイズ 形式 必須
アイコン 128x128 PNG 必須
プロモーション画像(小) 440x280 PNG/JPEG 推奨
プロモーション画像(大) 920x680 PNG/JPEG 推奨
マーキー画像 1400x560 PNG/JPEG 任意
スクリーンショット 1280x800 または 640x400 PNG/JPEG 最低1枚

スクリーンショットは最大5枚まで登録できます。以下のシーンを撮影しましょう。

  1. テキストをハイライトしている場面
  2. ポップアップUIの全体像
  3. 右クリックメニューの使用場面
  4. オプションページの設定画面
  5. エクスポート・インポート機能

ストアの説明文:

> Chrome Web Storeの説明文を日本語と英語で
  書いて。機能の説明、使い方、
  プライバシーへの配慮を含めて

Claude Code が生成する説明文の例です。

Web Highlighter — Webページのテキストをハイライト&保存

【機能】
- ワンクリックでテキストをハイライト
- 4色+カスタムカラー対応
- ハイライトの自動保存・復元
- 右クリックメニューからの操作
- データのエクスポート・インポート
- デバイス間の設定同期

【使い方】
1. ハイライトしたいテキストを選択するだけ
2. ポップアップで色の切り替えが可能
3. ページを再訪問すると自動的にハイライトを復元

【プライバシー】
すべてのデータはお使いのブラウザにローカル保存されます。
外部サーバーへのデータ送信は一切行いません。

審査プロセスと注意点

拡張機能を提出すると、Google の審査チームによるレビューが行われます。

審査期間: 通常1〜3営業日ですが、初回の提出や権限が多い拡張機能は数週間かかることもあります。

よくある却下理由: - 権限の過剰要求: 使っていない権限が manifest.json に含まれている - 機能の不足: 説明文に書かれた機能が実装されていない - プライバシーポリシーの不備: ユーザーデータを扱うのにプライバシーポリシーがない - ブランドの侵害: 他社の商標を無断で使用している - マルウェアの疑い: 難読化されたコードや不審な外部通信

再審査: 却下された場合は、指摘事項を修正して再提出できます。却下の理由は Developer Dashboard にメールで通知されます。

Claude Code に審査対策を確認しましょう。

> Web Highlighterの manifest.json と
  コードをChrome Web Storeの
  審査基準に照らしてチェックして。
  却下されそうな箇所があれば指摘して

アップデートの公開

拡張機能を公開した後、機能追加やバグ修正でアップデートする方法を確認します。

  1. バージョン番号を更新: manifest.json の version フィールドを更新します(例: 1.0.01.1.0)。
> manifest.jsonのバージョンを1.1.0に
  更新して。変更履歴も表示するように
  popup.htmlにバージョン表示を追加して
{
  "version": "1.1.0"
}
  1. 新しい ZIP をパッケージング: 修正後のコードを ZIP に圧縮します。

  2. Developer Dashboard から提出: 既存の拡張機能の「パッケージ」タブから新しい ZIP をアップロードします。

  3. 更新の説明: 変更内容を説明するテキストを入力します。ユーザーに自動的にアップデートが配信されます。

自動アップデート: Chrome は拡張機能のアップデートを数時間ごとに自動チェックします。chrome.runtime.onInstalled イベントの details.reason === 'update' で、アップデート後の初回起動を検知してマイグレーション処理を行えます。

chrome.runtime.onInstalled.addListener((details) => {
  if (details.reason === 'update') {
    const previousVersion = details.previousVersion;
    console.log(`アップデート完了: ${previousVersion} → ${chrome.runtime.getManifest().version}`);

    // バージョン固有のマイグレーション
    if (previousVersion === '1.0.0') {
      migrateFromV1();
    }
  }
});

コースのまとめ

全10回の講義を通じて、Chrome拡張機能の開発に必要な知識と技術を一通り習得しました。

  • Lecture 1-2: 拡張機能のアーキテクチャと manifest.json
  • Lecture 3: ポップアップ UI の構築
  • Lecture 4: コンテンツスクリプトによる DOM 操作
  • Lecture 5: Chrome Storage API によるデータ永続化
  • Lecture 6: Service Worker によるバックグラウンド処理
  • Lecture 7: コンポーネント間のメッセージ通信
  • Lecture 8: 外部 API との連携
  • Lecture 9: オプションページの実装
  • Lecture 10: Chrome Web Store への公開

Claude Code を使うことで、複雑なコードの生成やデバッグを効率的に行えました。今後も新しい機能のアイデアがあれば、Claude Code に相談しながら拡張機能を進化させていきましょう。

演習問題

  1. パッケージング: Web Highlighter を ZIP にパッケージングし、別の Chrome プロファイルで読み込んで正常に動作することを確認してください。
  2. スクリーンショット作成: Chrome Web Store 用のスクリーンショットを5枚撮影してみましょう。機能の魅力が伝わる構図を考えてください。
  3. 説明文の多言語化: Claude Code に英語版の説明文も作ってもらい、国際的な配布に備えましょう。
  4. 次のステップ: Claude Code に「Web Highlighterに追加できる機能のアイデアを10個提案して」と聞いてみましょう。次のバージョンの開発計画を立ててください。

参考リンク