iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

Chrome拡張機能のwebRequestAPIを使って通信を確認してみる

■はじめに

こんにちは。

株式会社iimonでフロントエンドを担当している白水です。

今回は「ChromeExtensionで使えるwebRequestAPI」を使って通信を確認する方法について書いていきたいと思います。

■ChromeExtensionAPIとは何か?

ChromeExtension(拡張機能)には、ChromeExtensionで使えるAPIがあります。

例えば、

  • chrome.bookmarks API は、chromeでブックマークしたら何かする
  • chrome.i18nAPIは、ChromeExtensionを国際化対応させる
  • chrome.tabsAPIは、ブラウザのタブシステムを操作する

などがあります。

これら含めたExtensionAPIを使うことで、ChromeExtensionだからできることが実現できます。

API リファレンス  |  Chrome for Developers

その中でも、今回は「webRequestAPI」を使ってみます。

chrome.webRequest  |  API  |  Chrome for Developers

■環境構築

リポジトリをCloneする

// SSH
❯ git clone git@github.com:shiramizu-junya/iimon-tech-blog.git

// HTTPS
❯ git clone https://github.com/shiramizu-junya/iimon-tech-blog.git

まずは、環境構築のためサンプルリポジトリをCloneしてきます。

今回は2024-07-22-chrome-webrequest-api-sampleディレクトリを使います。

npm installを実行して、パッケージをインストールします。

npm run startを実行して、webpack-dev-serverを起動します。

dist配下にファイルが吐き出されたと思います。

それぞれ、何がどこに吐き出されているかは、上記画像の線を確かめてください。

◆package.jsonの説明

"devDependencies": {
    "@types/chrome": "^0.0.268",
    "clean-webpack-plugin": "^4.0.0",
    "copy-webpack-plugin": "^12.0.2",
    "ts-loader": "^9.5.1",
    "typescript": "^5.5.3",
    "webpack": "^5.93.0",
    "webpack-cli": "^5.1.4",
    "webpack-dev-server": "^5.0.4"
}
  • "@types/chrome":ChromeExtensionAPIの型定義ファイル
  • "clean-webpack-plugin":ビルドするときにdist配下のファイルを1度削除するために使うPluginです。
  • "copy-webpack-plugin"manifest.jsoniconなどをdist配下にまるっとコピーするために使うPluginです。
  • "ts-loader":TSをJSに変換するものです。
  • "webpack""webpack-cli""webpack-dev-server":Webpackに関わるライブラリです。

◆webpack.config.jsの説明

// webpack.config.js

const path = require('path');
const webpack = require('webpack');
const CopyPlugin = require("copy-webpack-plugin");
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
    // 起点となるエントリーポイントのファイル
    entry: {
        content_script: './src/index.ts',
        background: './src/background/background.ts',
    },
    // dist配下に「content_script.js」と「background.js」という名前で吐き出しなさい
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js',
    },
    // moduleのimport時に拡張子を省ける設定(今回使わないです)
    resolve: {
        extensions: ['.ts', '.js'],
    },
    // ソースマップを有効にする設定
    devtool: 'inline-source-map',
    // webpack-dev-serverの設定
    devServer: {
        // staticファイルをどこのパスから提供するかの設定
        static: {
            directory: path.resolve(__dirname, 'dist'),
        },
        // ファイルに変更があったら、メモリ上だけでなく、dist配下のファイルも書き換える設定
        devMiddleware: {
            writeToDisk: true,
        },
    },
    // 「.ts」ファイルを「.js」ファイルに変換するときに「ts-loader」を使いなさい
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/,
            },
        ],
    },
    // WebpackのPlugin関係
    plugins: [
        // ファイルに変更があったら、既存のdistファイルの中身を消す設定
        new CleanWebpackPlugin(),
        // ビルドの進捗度を表示してくれるPlugin
        new webpack.ProgressPlugin(),
        // 「manifest.json」や「public」配下のファイルはビルド不要なので、そのままdist配下の吐き出しなさいというやつ
        new CopyPlugin({
            patterns: [
                {
                    from: path.resolve(__dirname, './manifest'),
                    to: path.resolve(__dirname, './dist')
                },
                {
                    from: path.resolve(__dirname, './public'),
                    to: path.resolve(__dirname, './dist')
                },
            ]
        })
    ],
};

ビルドの設定は、上記のようになっています。

詳細は割愛します。

Webpackを使ってバンドルしてみる - iimon TECH BLOG

npmとwebpackに触れてみる - iimon TECH BLOG

◆manifest.jsonの説明

// manifest.json

{
    "name": "WebRequestTestSample",
    "version": "1.0.0",
    "description": "Chrome Extension WebRequest API Test",
    "manifest_version": 3,
    "background": {
        "service_worker": "background.js",
        "type": "module"
    },
    "content_scripts": [
        {
            "js": [
                "content_script.js"
            ],
            "matches": [
                "https://www.google.co.jp/*"
            ]
        }
    ],
    "permissions": [
        "webRequest",
        "tabs"
    ],
    "icons": {
        "16": "./icons/16_fox.png",
        "48": "./icons/48_fox.png",
        "128": "./icons/128_fox.png"
    },
    "host_permissions": [
        "https://www.google.co.jp/*"
    ]
}
  • nameversiondescriptionmanifest_version拡張機能の設定
  • background:裏側で動くJSファイル
  • content_scripts:表側で動くJSファイル
  • permissions:Extensionで使うAPIホワイトリスト
  • icons:Extensionで表示するアイコン
  • host_permissions:特定のドメインやURLへのアクセスを要求するために使用されます。拡張機能がどのウェブサイトとやりとりできるかを制御します。

Chrome拡張機能の概要から公開まで(ManifestV3対応) ~概要編~

◆ChromeExtensionのインストール

拡張機能」のページ(chrome://extensions/)に移動して、

ディベロッパーツール」→「パッケージ化されていない拡張機能を読み込む」→「distファイルを選択して読み込む」と、狐マークの拡張機能が追加されます。

■content_script.jsとbackground.jsが正しく動作しているか確認

◆content_script.jsの実行確認

「googlemap」にアクセスすると、content_script.jsが実行されていることがわかります。

◆background.jsの実行確認

background.jsの実行を確認する場合は、Service Workerをクリックします。

別Windowでconsoleが起動します。

background.jsに書いているコードが実行されています。

■background.jsで通信を確認してみる

// src/background/background.ts

const handleWebRequestCompleted = (details: chrome.webRequest.WebResponseCacheDetails) => {
    console.log(details);
}

chrome.webRequest.onCompleted.addListener(
    handleWebRequestCompleted,
    { urls: ['https://www.google.co.jp/search?*'] }
);

src/background/background.tsに上記コードを記載します。

https://www.google.co.jp/search?*へのリクエストがonCompleted(完了したら)、handleWebRequestCompleted関数を実行するという内容になっています。

これで、GoogleMapで適当に検索すると、background.jsでログが取得されます。

このオブジェクトのURLがリクエストが投げられた先のURLになります。

■content_script.jsに通信の情報を渡してみる。

// src/index.ts

// ①
chrome.runtime.sendMessage('WebRequestInterceptStart', (response) => {
    console.log('=============================');
    console.log(response);
    console.log('=============================');
});

// ②
chrome.runtime.onMessage.addListener((message) => {
    if (message.type === 'webRequestCompleted') {
        console.log('Web request completed:', message.details);
    }
});
// src/background/background.ts

// ③
const getActiveTabId = () => {
    return new Promise((resolve: (value: number) => void, reject: (reason: Error) => void) => {
        chrome.tabs.query({ active: true, currentWindow: true }, (tabs: chrome.tabs.Tab[]) => {
            if (tabs[0]?.id) {
                resolve(tabs[0].id);
            } else {
                reject(new Error('TabIDの取得に失敗しました'));
            }
        });
    });
};

// ④
const handleWebRequestCompleted = async (details: chrome.webRequest.WebResponseCacheDetails) => {
    try {
        const activeTabId = await getActiveTabId();
        if (activeTabId) {
            chrome.tabs.sendMessage(activeTabId, {
                type: 'webRequestCompleted',
                details: details,
            });
        }
    } catch (error: any) {
        console.error(error.message);
    }
};

// ⑤
const setWebRequestListener = () => {
    chrome.webRequest.onCompleted.addListener(handleWebRequestCompleted, {
        urls: ['https://www.google.co.jp/search?*'],
    });
};

// ⑥
chrome.runtime.onMessage.addListener((message: string, sender, callBackFunc) => {
    if (message === 'WebRequestInterceptStart') {
        setWebRequestListener();
        callBackFunc({ status: 'chrome.webRequest.onCompletedイベントをセットしました' });
    }
});

メッセージパッシングをsrc/background/background.tsの⑥の処理で受け取ります。

⑥のsetWebRequestListener();でwebRequestのイベントをセットしています。

⑤で指定した'https://www.google.co.jp/search?*’へのリクエストが完了すると、

④のhandleWebRequestCompleted関数が実行されます。

④の引数detailsの中に通信の中身が入ってきます。

③のgetActiveTabId関数で現在のwindow(currentWindow: true)でアクティブなタブ(active: true)の情報を取得して、アクティブなタブのIDを返しています。

④でアクティブなタブに対して、{ type: 'webRequestCompleted', details: details, }でdetails(通信情報)を返しています。

src/index.tsの②でsrc/background/background.tsからのメッセージパッシングを受け取って、通信の情報をcontent_script.js側で出力しています。

これで、content_script.js側でどんな通信が走ったのか取得することができます。

■まとめ

検索条件が複数あって1つ1つのパラメーターを調べるのは無理だけど、通信の中身(ex. ユーザーがどんな検索をしたのか)知りたいなどのときに便利かなと思いました。

他にもChromeExtensionならではのAPIがあるので、試していければなと思います。

ここまで読んでくださってありがとうございました。

また、弊社ではエンジニアを募集しております。

ぜひカジュアル面談でお話ししましょう!

ご興味ありましたら、ご応募ください!

Wantedly / Green