ユーザーのかわりに連打してくれる拡張機能を作った

## はじめに
これはクソアプリ Advent Calendar 2021の19日目の記事です。

それはクソアプリ初日のカレンダーを見ていた時のこと。

「電卓成長させるアプリ面白い！ただクリックし続けるのめんどい、、、。そうや！」

ということで、カレンダーに初参戦するために、自分でクリックする代わりに、自動でしてくれるChrome拡張をつくることになりました。

[電卓の成長を体験できるクソアプリについてはこちら](https://qiita.com/de_teiu_tkg/items/5d878f01207f408ee919)

## 使い方

1. 連打したい要素を右クリック
1. [連打する]を選択
1. あとは待つだけ

![renda.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265263/a97e81e2-ecc8-86a8-c03e-0415446ce1b9.gif)
)


## 技術スタック

- HTML
- CSS
- JavaScript

[コード](https://github.com/Hayato07/renda_chrome_extension)

もともとReact+TypeScriptで、拡張機能を作成しようとしたのですが、拡張機能の作成、TypeScriptは全くの未経験だったので速攻で挫折しました。

「とにかく動くものを作ってから、型付けだったり、リーダブルなコードを作ったりしよう」そう考えた私は、本当に一番初歩的な技術スタックを用いて作成することにしました。それでも非常に詰まりました。

## 実装

Chrome拡張の基礎知識(content sctipt, action, pageという3つの概念)をざっくり学んだ後、以下のような流れで作成いたしました。(manifest_version:3で動作するように作成しています)

- 連打に関する設定
- コンテキストメニューの作成
- メニュー選択時の要素取得、連打実行

### 連打に関する設定
```json:manifest.json
    "action": {
        "default_title": "連打マン",
        "default_popup": "popup.html"
    },
    "permissions": [
        "storage"
    ],
```

もともとbrowser_action(全ページ)とpage_action(特定ページ)に分かれていましたが、manifest_version:3でactionに統一されました。
permissionsについては、後ほど説明します。

```html:popup.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="utf-8" />
    <title>Rendaman</title>
</head>

<body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root">
        <p>設定</p>
        <div>
            <label>
                連打回数:
                <input type="number" id="renda_count" />
            </label>
            <label>
                連打間隔(ms):
                <input type="number" id="renda_interval" />
            </label>

            <button id="renda_set">設定する</button>
        </div>
    </div>
    <script src="popup.js"></script>
</body>

</html>
```

```js:popup.js
// 読み込み時に、既存の設定を反映
window.onload = function () {
    let count = document.querySelector('#renda_count');
    let interval = document.querySelector('#renda_interval');
    chrome.storage.local.get(['renda_count', 'renda_interval'], function (result) {
        count.value = result.renda_count ? result.renda_count : 100;
        interval.value = result.renda_interval ? result.renda_interval : 100;
    });
}

const clickEvent = () => {
// 設定ボタンクリック時に、データをローカルストレージに保存
    const count = document.querySelector('#renda_count').value ? document.querySelector('#renda_count').value : 100;
    chrome.storage.local.set({ renda_count: count });

// 設定ボタンクリック時に、データをローカルストレージに保存
    const interval = document.querySelector('#renda_interval').value ? document.querySelector('#renda_interval').value : 100;//ms
    chrome.storage.local.set({ renda_interval: interval });
}

document.querySelector("#renda_set").addEventListener('click', clickEvent)
```

###　コンテキストメニューの作成

```json:manifest.json
    "host_permissions": [
        "*://*/*"
    ],
    "permissions": [
        "contextMenus",
        "tabs",
        "activeTab",
        "storage"
    ],
    "background": {
        "service_worker": "background.js"
    }
```

permissionsは、chromeが提供するAPIを利用するために必要な記述です。今回は、
- contextMenus (chrome.contextMenus APIへのアクセス権)
- tabs (chrome.tabs APIへのアクセス権)
- activeTab(表示中のタブへのアクセス権限)
- storage (chrome.storage APIへのアクセス権)



```js:background.js
chrome.runtime.onInstalled.addListener(function () {
    // メニュー追加
    chrome.contextMenus.create(
        {
            id: "rendaman",
            title: "連打する",
            contexts: ["all"] // すべての要素で右クリック時に表示
        }
    )

    // // 追加したメニューがクリックされた時
    chrome.contextMenus.onClicked.addListener(() => {
        // 表示中のタブを取得し、content scriptにメッセージを送る
        chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
            chrome.tabs.sendMessage(tabs[0].id, { action: "rendaman" });
        });
    });
})

```

### メニュー選択時の要素取得

メニュー選択時の要素をchrome.contextMenus.onClickedイベントの引数から取得することは、残念ながらできません。

なので今回は、content script側で右クリックを行った時の要素を取得しておき、実際にメニューがクリックされた場合その要素を連打するようにしました。

```json:manifest.json
"content_scripts": [
        {
            "matches": [
                "<all_urls>"
            ],
            "js": [
                "content.js"
            ],
            "all_frames": false,
            "match_about_blank": true
        }
    ]
```

```js:content.js

let clickedEl = null;
// 右クリックでメニューを開いた際の要素を取得
document.addEventListener("contextmenu", function (event) {
    clickedEl = event.target;
}, true);

// メッセージを受け取った際の動作を指定
chrome.runtime.onMessage.addListener(rendaman);

function rendaman(request, sender, sendResponse) {
    if (!clickedEl) return false;

    // 想定のメッセージかをチェック
    if (request.action != "rendaman") return false;

    let renda_count, renda_interval, clickInterval;

    // 既存の連打処理があれば削除
    if (clickInterval) {
        clearInterval(clickInterval);
    }

    // ストレージから連打回数、連打間隔に関するデータを取得し、連打を実行
    chrome.storage.local.get(['renda_count', 'renda_interval'], function (result) {
        renda_count = result.renda_count ? result.renda_count : 100;
        renda_interval = result.renda_interval ? result.renda_interval : 100;

        clickInterval = setInterval(function () {
            if (renda_count <= 0) {
                clearInterval(clickInterval);
                // 連打がうまく実行されるとメッセージに対するレスポンスをかえす
                sendResponse({ message: "renda completed!!" });
                return;
            }

            clickedEl.click();
            renda_count--;
        }, renda_interval);
    });

    return true;
}
```

以上で指定回数、間隔で要素を連打する拡張の完成です！出来上がってみると、案外簡単でしたが、作成中は知らない内容ばかりだったので、さまよっていました。

### 詰まりポイント① manifest_version

chrome拡張を作る際に指定するmanifest_versionは、これまで2だけでしたが、chrome88のリリース時にで3が導入ました。

これにより、versionによる挙動の違いに混乱しました。

### 詰まりポイント② Message Passing

[Simple one-time requests](https://developer.chrome.com/docs/extensions/mv3/messaging/)

```js
chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
  console.log(response.farewell);
});
```

上記のURLの通り、やっているのに何度試してもErrorがでるなと思っていたら、extensionからcontent scriptに送りたい場合には下のコードを使いなさいと書いていました。。。。

```js
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
  chrome.tabs.sendMessage(tabs[0].id, {greeting: "hello"}, function(response) {
    console.log(response.farewell);
  });
});
```
### 詰まりポイント② 何度もEventが発火する

右クリックで、メニューを選択したときにメッセージを送るようにしていたのですが、受け取る側のconsole.logがなぜか複数回表示されることがありました。

原因は、manifest.jsonのall_frames:trueにしていたためでした。
表示ページのすべてのiframeでもcontent scriptを実行するかを指定するものです。
今回は、iframeは必要なかったので、offにしました。

```json:manifest.json
"content_scripts": [
        {
            "matches": [
                "<all_urls>"
            ],
            "js": [
                "content.js"
            ],
            "all_frames": false,
            "match_about_blank": true
        }
    ]
```


## 参考

- [API Reference
](https://developer.chrome.com/docs/extensions/reference/)
- [Chrome Extension の作り方 (その1: 3つの世界)](https://qiita.com/sakaimo/items/416f36db1aa982d8d00c)
- [Chrome拡張の開発方法まとめ　その1：概念編
](https://qiita.com/k7a/items/26d7a22233ecdf48fed8)