Chrome拡張機能を作ってみる（基礎）

## はじめに

Chromeを使用していて、ましてエンジニアであれば拡張機能を使わない人はいないでしょう。気付いたら100個近く入れていた、なんて人もいるんじゃないでしょうか。ホント便利ですよね。でもあまり入れすぎると重くなるので注意です（自分みたいにタブ100個とか開く人は特に…

ユーザのみなさんも「すごい・・」と溜め息が出るようなものから、手作り感のある見た目のものまで、色々見てきたのではないかと思いますが、その拡張機能、どうやって実装されているか気になりませんか？

これ、ただ作るだけなら簡単に作れちゃいます。もちろん凝ったものを作ろうと思えば度知識と経験が必要ですが、まぁアプリと一緒です。そんな事はその時考えたらいいので、まずは作ってしまいましょう！

## インストールまで

### manifest.jsonを作成

フォルダを一つ作成して、そこに manifest.json というファイルを作成してください。中身は以下のような感じで。

```json
{
  "name": "はじめての拡張機能",
  "description": "機能を拡張するものです",
  "version": "1.0",
  "manifest_version": 3
}
```

### インストール

ブラウザで[Chrome拡張機能](chrome://extensions/)を開きます。右上にあるデベロッパーモードをONにして、「パッケージ化されていない拡張機能を読み込む」ボタンを押したら、先ほどの開発フォルダを選択してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31281/7bf56a2d-5352-b3c7-e74e-df6dce39c746.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31281/969c7248-7608-a3e9-763b-818128d4e9f1.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31281/b0825a95-18ac-af96-c70e-31a745b5f9a2.png)

拡張機能ページに拡張機能が追加されましたか？
おめでとうございます！これで晴れてChrome拡張機能開発者の一員です。

## 拡張機能ボタンのポップアップを作る

全く機能がないとあまりに寂しいので、まずは拡張機能ボタンを押した時に表示されるポップアップを作成しましょう。

### ファイルを準備

フォルダにpopup.htmlを作成してください。内容は適当で構いません。popup.htmlからは通常のhtmlと同じようにjsやcssを読み込めるので、必要であればlinkタグとscriptタグを記述します。jQueryやVue.jsなど使い慣れたライブラリがある人は使用すると楽になります。

```html:popup.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>popup</title>
  <link rel="stylesheet" href="/css/popup.css">
  <script src="/js/popup.js" defer></script>
</head>
<body>
  <p>はじめてのpopup</p>
</body>
</html>
```

```js:js/popup.js
console.log('popup.js');
```

```css:css/popup.css
* {
  font-weight: bold;
}
```

### マニフェストに追加

manifest.jsonのトップ階層に以下を追記します。

```json:manifest.json
"action": {
  "default_popup": "popup.html"
}
```

### 拡張機能をリロード

拡張機能のページに更新ボタンがあると思うのでクリックしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31281/c3ef76a3-1cf3-45f2-0763-33ca710a3495.png)
無事リロードできたらOKです。もしmanifest.jsonや初期実行されるjsにミスがある場合はエラーダイアログが出ます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31281/64304492-c551-edd6-cbd3-8109af4a9778.png)

### ポップアップを表示

リロードしたものの画面の変化がないのは、隠れているからです。拡張機能の並びにあるパズルのようなボタンを押して、今回作った「はじめての拡張機能」をクリックすると・・・popup.htmlが表示されましたね？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31281/4e6acd36-4b48-7a36-6694-dedcfc234986.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31281/8412d617-1a74-db73-d40a-bfc209b081a5.png)

拡張機能名の横にあるボタンから、拡張機能をピン留めできます。ピン留めされた拡張機能は常時表示されるので、開発時はピン留めしておきましょう。

### ポップアップのデバッグ

拡張機能アイコンを右クリックすると「ポップアップの検証」というメニューがあるのでクリックしてください。ポップアップ表示と同時に開発者ツールが開いて、Elementやコンソールなどが見られます。

## ポップアップにアイコン追加

拡張機能ボタンには、拡張機能名の頭文字が表示されていると思います。この手順通りなら `「は」` と表示されているでしょうか。他の拡張機能はちゃんとしたアイコンが使われているので、逆に目立ってわかりやすいですが、せっかくなのでアイコン表示させてみましょう。

### 画像を準備

まずはPNG画像を用意します。正方形で、サイズは16px、32px、48px、128pxいずれかが必要です。JPGやSVGは使えません。imagesフォルダを作って、そこにicon-128.pngなどの名前で格納したとします。

ちなみにアイコンのガイドラインはこちらに記載されています。
[Design the user interface - Chrome Developers](https://developer.chrome.com/docs/extensions/mv3/user_interface/#icons)

### マニフェストに追加

actionにdefault_iconの項目を追加しました。今回4サイズとも作成しましたが、どれか一つあれば大丈夫です。（その場合、manifest.jsonも該当の1行のみでOKです）

```json
    "action": {
        "default_popup": "popup.html",
        "default_icon": {
            "16": "/images/icon-16.png",
            "32": "/images/icon-32.png",
            "64": "/images/icon-64.png",
            "128": "/images/icon-128.png"
        }
    }
```

### アイコン確認

リロードボタンを押すと、アイコンが変わるはずです。それっぽくなってきましたね！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31281/72fc44c1-d198-30f8-cd4b-e382e0d3bb50.png)

### 拡張機能ページのアイコン追加

拡張機能ボタンのアイコンを追加しましたが、ついでなので同じ画像を使って拡張機能ページに表示されるアイコンも追加してしまいましょう。manifest.jsonに以下の項目を追記します。こちらはactionの中ではなく、トップ階層に追加してください。

```json:manifest.json
    "icons": {
        "16": "/images/icon-16.png",
        "32": "/images/icon-32.png",
        "48": "/images/icon-48.png",
        "128": "/images/icon-128.png"
    }
```

拡張機能をリロードしたらアイコンが変わりました＼(^O^)／
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31281/5d186566-79d9-d87c-7eba-9872e2fc6d26.png)

## マニフェストバージョンについて

閑話休題。

Chrome拡張機能にはマニフェストのバージョンがあり、現在の最新はVersion3(以後V3)となっています。V2とV3で、manifestの記述名や必要項目が結構違っています。かなり込み入った話になるので、気になる方はまた調べてみてください。

世に出ている記事はV2時代のものも多いので、そのままだと動かないことも多いです。新しい情報を調べる時は単語だけでなく「V3」もつけて検索すると変更点などが見つかりやすくオススメです。

2022年8月1日時点で、EdgeはV3対応済み、FireboxはV3対応中（開発者プレビュー版などで使用可能）となっています。

## Service Workerの追加

V2までバックグラウンドページやイベントページと呼ばれていたもので、V3からService Workerに置き換えられました。厳密には違うものですが、書く内容はそれまでとほぼ同じです。Chrome拡張機能がインストールされてから裏で黙々と仕事をしてくれる、コアとなるスクリプトです。

### スクリプトとマニフェストの追加

js/background.jsを作成して、以下のように記入してください。

```js:js/background.js
console.log('in background');
```

マニフェストはトップの階層に追加します。

```json:manifest.json
    "background": {
        "service_worker": "/js/background.js"
    }
```

拡張機能をリロードすると、「ビューを検証 Service Worker」のようなリンクが表示されたかと思います。クリックすると開発者ツールが開き、コンソールには「in background」が出力されているのが分かります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31281/258e4f5c-0f16-7336-16f3-08a571b4e769.png)

Service Workerがどんなものかは、[Chrome Extension の作り方 \(その1: 3つの世界\) \- Qiita](https://qiita.com/sakaimo/items/416f36db1aa982d8d00c#3%E3%81%A4%E3%81%AE%E4%B8%96%E7%95%8C-%E3%81%AE%E3%81%A7%E3%81%8D%E3%82%8B%E3%81%93%E3%81%A8%E3%81%AE%E9%81%95%E3%81%84)に詳しいので、ぜひ見てみて下さい。自分もこちらで概念を学びました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31281/79ea5dbb-b173-304b-1615-518677bb3ddc.png)
ちなみにポップアップを検証すると、コンソールに「in background」と出ています。ポップアップの領域でもService Workerが動いているので、後述するメッセージ通信の際には、その点だけ念頭に置いて注意してください。

## Content Scriptsを設定

Content Scriptsは各ページで動作するスクリプトです。Chromeはブラウザで、その拡張機能なので、各ページに何らかの動作をさせたいですよね。それを担うものになります。

### 設定方法

以下のようなcontent.jsを用意し、manifest.jsonにcontent_scriptsの記述を追加してください。matchesは動作対象のURIを指定します。<all_urls>は全てのページが対象になります。

```js:js/content.js
console.log('content.js');
document.body.style.backgroundColor = '#fcc';
```

マニフェストも追加します。トップ階層に記述してください。

```json:manifest.json
    "content_scripts": [
        {
            "matches": [
                "<all_urls>"
            ],
            "js": [
                "/js/content.js"
            ]
        }
    ]
```

拡張機能をリロードして、適当なタブを開いてリロードすると背景色がピンクになったはずです。さすがに邪魔すぎるので、動作だけ確認できたらbackgroundColorの行は削除して拡張機能をリロードしておきましょう。

このようにContent Scriptsでは、開いているタブのDOMにアクセスすることが可能です。jsは複数指定することが出来るので、jQueryを読み込んで、それを利用することも可能です。

また、Content Scriptsは各タブ上で動作するスクリプトなので、console.logの出力は、いつも通りにタブの開発者ツールから確認できます。

matchesに指定できるパターンは以下で確認できます（fileプロトコルの記載もあります）
[Match patterns - Chrome Developers](https://developer.chrome.com/docs/extensions/mv3/match_patterns/)

### エラー確認

試しに `window.document.body = null;` の記述を追加して拡張機能をリロードし、適当なタブを開いてページにアクセスすると、拡張機能ページに「エラー」ボタンが出てエラーを確認できます。

エラーは一度出ると自動では消えないので、手動で削除してください。リロードしてもエラーが消えないと勘違いして、スクリプト修正とリロードを繰り返して時間を溶かすのは切ない‥

## optionページ

ポップアップと同様にhtmlファイルを用意します。jsやcssを呼べるのも同じです。適当な内容でoptions.htmlなどの名前で作成してください。

manifest.jsonのトップ階層に追記します。

```json:manifest.json
    "options_page": "options.html"
```

拡張機能をリロードしたら、拡張機能アイコンを右クリックすると「オプション」があります。クリックでオプションページが開きます。オプションページは単体のタブとしてページが開くため、通常の方法で開発者ツールが見られます。

## 各領域間のメッセージ通信

Content Scripts、Action（ポップアップ)、Service Workerでのやり取りはメッセージで行います。

[Chrome Extension の作り方 (最終話: メッセージパッシング) - Qiita](https://qiita.com/sakaimo/items/5e41d6b2ad8d7ee04b12#%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%83%91%E3%83%83%E3%82%B7%E3%83%B3%E3%82%B0%E3%81%AE%E9%96%A2%E4%BF%82%E5%9B%B3)の図が分かりやすいです。先ほど紹介した記事もこのシリーズですが、Chrome拡張機能の概念的なことを最初に理解するのにとても有用なので、興味があればその1から読んでみて下さい。

メッセージ通信のAPIはchrome.runtime.sendMessageとchrome.tabs.sendMessageの2つあります。記事にもありますが、Content Scripts宛ては、タブ内にあるページに仕込んだスクリプトだからtabsで、それ以外はruntimeで、その理解でOKです。

### 通信テスト

説明だけではピンと来ないので実際に書いてみましょう。素のjsだと若干つらいのでjQueryを使うことにしました。

popup.htmlにボタンとjQueryを追加。それぞれ良き場所へ。

```html:popup.html
<script src="/js/jquery-3.6.0.min.js" defer></script>
<button id="send" type="button">メッセージ送る</button>
```

popup.js にボタン押下でメッセージ送信処理を追加。コメント多めで少し見づらいですが、jQueryのAjax通信と書き方は似ています。sendMessageでメッセージを送って、送信先からのレスポンスがthenの第一引数、エラー時の処理が第二引数に入ります。

```js:js/popup.js
console.log('popup.js')

$('#send').on('click', function (e) {
  // chrome.runtime.sendMessageでruntime宛てにメッセージを送信
  chrome.runtime.sendMessage({
    greeting: "Greeting from the content script"
  }).then(
    function (message) { // レスポンス受信時
      // messageは 受信側で sendResponse関数に渡した引数が返ってきます
      console.log(`Received response:  ${message.response}`);
    },
    function (error) { // エラー時
      console.log(`Error: ${error}`);
    }
  )
});
```

background.jsにメッセージ受信時の処理を追加

```js/background.js
console.log('in background');

/**
 * メッセージ受信時のアクションを定義
 * @param {object} request sendMessageの引数が入ってきます
 * @param {object} sender 送信元の情報オブジェクト id, origin, url
 * @param {function} sendResponse レスポンスを返すための関数
 */
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
  console.log(`Received message : ${request.greeting}`);
  sendResponse({response: "Response from background script"});
});
```

popupが狭くて見づらいのでcssに記述追加します。

```css:css/popup.css
body {
    min-width: 200px;
}
```

### メッセージ確認

拡張機能をリロードして、ビューを検証の「Service Worker」リンクをクリックし、検証ウインドウを開いておきます。拡張機能ボタンを押して、「メッセージ送る」ボタンを押すと、検証ウインドウにメッセージが表示されたのではないでしょうか。

### メッセージ受信の注意

メッセージをruntime宛てに送った場合、Service Workerだけでなく、オプションページなどにも同時に送られます。メッセージ送信時のオブジェクトにtypeプロパティなどのラベルになるものを付けておいて、必要な処理だけ行えるように実装しましょう。

## タブ(Content Scripts)とのメッセージ通信

### 拡張機能ボタンの動作変更

タブとのメッセージ通信は chrome.tabs.sendMessageで行います。

今までのやり方だと拡張機能ボタンを押すとポップアップが表示されますが、ポップアップ表示ではなく chrome.action.onClicked が発火するように変更してみます。（念のため、ポップアップのアクションからタブにメッセージを送ることもできます）

具体的には下記のdefault_popupの行を削除します。

```json:manifest.json
    "default_popup": "popup.html"
```

### メッセージ送受信

Service Workerに拡張機能ボタンクリックのイベントリスナを登録します。
拡張機能ボタンを押した時に開いていたタブがコールバックの引数として渡るので、chrome.tabs.sendMessageの第1引数としてtab.idを渡せば、該当タブのcontent scriptにメッセージが送られます。

```js:js/background.js
// 拡張機能ボタンを押下時
chrome.action.onClicked.addListener(function (tab) {
  console.log('Received action.onClicked message', tab);

  // メッセージへの送信データ(データ型は任意)
  const request = {
    action: 'browserAction.onClicked'
  };

  // 拡張機能ボタンを押したときのタブ(=現在開いているタブ)にメッセージを送信
  chrome.tabs.sendMessage(tab.id, request, function (response) {
    console.log(`Received response: ${response.message}`);
  });
});
```

Content Scriptsでの受信はこちら。送る側はchrome.tabsで送りますが、受信は先ほどまでと同様にchrome.runtime.onMessageで受ける点だけ注意。その他、全体的には先ほどまでと似た実装です。

```js/content.js
console.log('content.js')

/**
 * メッセージ受信時のアクションを定義
 * @param {object} request sendMessageの引数が入ってきます
 * @param {object} sender 送信元の情報オブジェクト id, origin, url
 * @param {function} sendResponse レスポンスを返すための関数
 */
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
    console.log(`Received message `, request);
    sendResponse({message: "Response from content script"});
});
```

### 動作確認

拡張機能をリロードして、適当なタブを開いてページをリロードします。（Content Scriptsはページ読み込み時に読み込まれるため、リロードしないと古いままです）
拡張機能ボタンをクリックすると、Service Worker側のコンソールと、ページ(Content Scripts)側のコンソールにconsole.logのメッセージが表示されると思います。

タブへのメッセージ送信の注意点として、拡張機能ボタンを押した際のタブがmanifest.jsonのmatchesに一致しない場合、Content Scriptsが動作していない（読み込まれていない）ので、タブにメッセージを送るService Worker側で `Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.` のようなエラーが出ます。

また、拡張機能ページ内（chrome://extensions/配下）など一部動作しない場所があります。

## コンテキストメニューの追加

各ページの右クリック（コンテキストメニュー）を作成することもできます。通常、拡張機能のインストール時に1度だけ行うため、runtime.onInstalledのリスナーに処理を登録します。

background.jsに下記の記述を追加してください。

```js:background.js
// コンテキストメニュー初期化用の関数
const updateContextMenus = async () => {
  // 一度全てクリア
  await chrome.contextMenus.removeAll();

  const type = 'normal'; // 他に checkbox, radio, separator がある

  const parentId = chrome.contextMenus.create({
    id: "first-extension",
    type,
    title: "はじめての拡張機能",
    contexts: ["all"]
  });

  chrome.contextMenus.create({
    id: "first-extension-something",
    parentId,
    title: "何かしらのアクション",
    contexts: ["all"]
  });
};

// インストール時、再セットアップ時（拡張機能リロード）に実行されるよう、リスナーを登録
chrome.runtime.onInstalled.addListener(updateContextMenus);
chrome.runtime.onStartup.addListener(updateContextMenus);

// コンテキストメニューのクリックイベントリスナーを登録
chrome.contextMenus.onClicked.addListener((info, tab) => {
  switch (info.menuItemId) {
  case 'first-extension-something':
    console.log('fire something!');
    break;
  }
});
```

拡張機能ページで、いつものようにリロードではなく、右下のトグルボタンを一度オフにして、再度オンにすると再インストールになります。右クリックをして、コンテキストメニューに「はじめての拡張機能」がいたらOKです。（リロードでも追加されるようにしてありますが、念のため）

## その他の機能

[API Reference \- Chrome Developers](https://developer.chrome.com/docs/extensions/reference/)

詳しくは上記リファレンスを見て頂きたいですが、ChromeのAPIはタブ（作成や更新）、ブックマーク、ストレージ（設定値保存）、クリップボードなど、かなりの種類が用意されています。

## 使いどころ

自社サービスで公式拡張機能を作るような場合でなくても、ちょっとした便利ツールとしての出番は多いです。

例えば権限のチェックボックスがたくさん並んだ管理ページなどで、データを追加するたびに
大量のチェックを入れる場合など、ちょっとしたjsを書いて開発者ツールのコンソールから流したりするケースはないでしょうか。管理ページ自体に機能として入れるのは影響が大きい（管理者全員に使わせたくないなど）けれど、保守で使いたい場合などに一つ拡張機能を作っておくと、スクリプトをいちいち共有しなくても拡張機能に一元化され、実行も拡張機能ボタン一つで行えて便利です。

## 拡張機能を覗き見る

Chrome拡張機能は、ウェブストアからインストールしたものであっても、読める形（html,css,js)でインストールされます。読める‥とはいったもののほとんどは難読化されていますが、難読化復元ツール（下記サイト）などを使ってソースを見ると、ヒントになるものがあるかもしれません。
[DirtyMarkup Beautifier - Javascript Formatter, JS Tidy Up](https://www.10bestdesign.com/dirtymarkup/js/)

拡張機能は下記フォルダに保存されており、拡張機能ページのIDとフォルダ名が一致しています。

- Mac: /Users/<ユーザー名>/Library/Application Support/Google/Chrome/Default/Extensions
- Win: C:\Users\<ユーザー名>\AppData\Local\Google\Chrome\User Data\Default\Extensions

## 最後に

見てきたように、基本的にはWebの技術(html+js+css)とブラウザAPIの組み合わせで成り立っているので、フロントエンド技術者にはとっつきやすいですよね。VueやReactなどフレームワークを使っての開発もできるので、ぜひ沼にハマって便利な拡張機能を開発してみてください！