📌

ReactでChrome拡張機能を作るメモ

2024/05/21に公開

はじめに

Chrome拡張機能を作る機会があったのでその時のメモです。

ライブラリ選定

以下のライブラリを使用する。

  • React
  • TypeScript
  • Vite
  • Material-UI
  • prettier

選定基準は新しすぎず古すぎない。

プロジェクトの作成

プロジェクト作成は以下のサイトを参考に行う。

https://crxjs.dev/vite-plugin/getting-started/react/create-project

vite3がbeta版なので、vite2を使用する。

プロジェクトの設定

MUIは以下のサイトを参考にインストールする。

https://mui.com/material-ui/getting-started/installation/

prettierは以下のコマンドでインストールする。

npm i prettier -D

ファイル構成は以下のようにする。

.
├── manifest.json
├── package-lock.json
├── package.json
├── src
│   ├── background.ts
│   ├── content.ts
│   └── popup
│       ├── App.tsx
│       ├── index.css
│       ├── index.html
│       └── main.tsx
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
  • manifest.json: Chrome拡張機能の設定ファイル。このファイルには拡張機能の基本情報や、使用する権限、スクリプトの情報などを記述する。
  • src/background.ts: バックグラウンドスクリプト。バックグラウンドで動作し続け、イベントのリスニングや定期的な処理を行う。
  • src/content.ts: コンテンツスクリプト。特定のウェブページに注入され、そのページ内のDOMを操作します。
  • src/popup: ポップアップページのUIを作成するディレクトリ。Reactで作成します。ポップアップはブラウザのツールバーに表示されるUIです。

Tips

こまごまとしたTipsを記載する。

コンテンツスクリプトでページ内の変更を監視する

MutationObserverを利用することで、ページ内の変更を監視することができる。document.body内の変更を監視する場合は以下のようにする。

const observer = new MutationObserver((mutations) => {
    console.log("change detected in page");
});

observer.observe(document.body, {
  childList: true,
  subtree: true,
  attributes: true,
  characterData: true,
});

参考: https://developer.mozilla.org/ja/docs/Web/API/MutationObserver

コンテンツスクリプトでページのロード時に処理を実行する

ページのロード時に処理を実行する場合は以下のようにする。

window.addEventListener("load", () => {
    console.log("page loaded");
});

ただ、上記の処理は、ページのロードが完了した時に実行される。ページ内のDOMが完全に読み込まれる前に処理が実行される場合がある。ページ内に特定のDOMが存在するまで待機する場合は以下のようにする。

const observer = new MutationObserver((mutations, observer) => {
    const targetElement = document.querySelector(".target");
    if (targetElement) {
        console.log("target element found");
        observer.disconnect();
    }
});
observer.observe(document.body, {
  childList: true,
  subtree: true,
});

上記の例では、.targetというクラス名を持つ要素がDOMに追加されるまで待機する。要素が追加されたら、MutationObserverを停止する。

Storage APIを利用してpopupからcontentスクリプトにデータを渡す

popupの状態をStorageに保存しつつ、contentスクリプトにデータを渡すことができる。以下のようにする。

popup側でデータを保存する。

chrome.storage.local.set({ key: "value" });

contentスクリプトでデータを取得する。一つ目の処理は初期値を取得する処理、二つ目の処理はデータが変更された時の処理。こうすることで、popupからcontentスクリプトに即座にデータを渡すことができる。

chrome.storage.local.get(["key"], (result) => {
    console.log(result.key);
});

chrome.storage.onChanged.addListener((changes, namespace) => {
    console.log(changes.key.newValue);
});

contentスクリプトで音声を再生する

contentスクリプトで音声を再生する場合は、以下のようにする。
まず、音声を再生するための初期化処理を行う。

let synth = null;
let voice = null;

function initSynth() {
    synth = window.speechSynthesis;
    voice = null;
    const VOICE_URI =  "Microsoft Nanami Online (Natural) - Japanese (Japan)"; // Edgeの日本語音声
    synth.onvoiceschanged = () => {
        const voices = synth.getVoices().filter((v) => v.voiceURI === VOICE_URI);
        if (voices.length == 0){
            console.error("voice not found");
            return;
        }
        voice = voices[0];
    };
}

onvoiceschangedイベントが発生した時に、音声を再生するためのvoiceを取得する。次に、音声を再生する関数を作成する。こうしないと、getVoices()で取得したvoiceがnullになる場合がある。

以下のようにして音声を再生する。

function speak(text) {
    if (synth == null || voice == null) {
        console.error("synth not initialized");
        return;
    }
    const utterText = new SpeechSynthesisUtterance();
    utterText.text = text;
    utterText.lang = "ja-JP";
    utterText.voice = voice;
    synth.speak(utterText);
}

毎回new SpeechSynthesisUtterance()でオブジェクトを生成する。使い回すと音声の再生速度よりも多くの音声を再生しようとするとした場合に再生されない場合がある。使い回さない場合は、内部でキューイングされるため、すべての音声を再生することができる。

contentスクリプトでの状態管理

contentスクリプトはコールバック関数を多用するため、グローバル変数を多用することになる。以下のようにして状態を一つにまとめる戦略がある。

const state = {
  synth: null,
  voice: null,
};

const updateState = (key, value) => {
  state[key] = value;
};

これ以上に複雑な状態管理が必要な場合は、Reduxを利用することを検討する。

バックグラウンドスクリプトからエラーを返す

バックグラウンドスクリプトを呼び出す場合、以下のようなフォーマットでエラーを返すことができる。

background側

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
    if (request.action === "Hello") {
        const name = request.payload.name;
        if (!name) {
            sendResponse({ success: false, error: "name is required" });
            return;
        }
        callHello(name)
            .then((data) => {
                sendResponse({ success: true, data });
            })
            .catch((error) => {
                console.error("Failed to call hello", error);
                sendResponse({ success: false, error: error.toString() });
            });

    }
    return true; // 非同期処理のためtrueを返す必要がある。これを忘れると正常に動作しない。
});

async function callHello(name) {
    hoge();
}

popup側

chrome.runtime.sendMessage({
    action: "Hello",
    payload: {
        name: "world",
    },
}, (response) => {
    if (response.success) {
        console.log(response.data);
    } else {
        console.error(response.error);
    }
});

successを用意して、エラーが発生した場合はerrorを返すことで、エラー処理を行うことができる。

おわりに

Chrome拡張機能を作る際のメモでした。

Discussion