Chrome拡張機能をReact + typescript で作る（manifest v3）

# Chrome拡張機能をReact + typescript で作る（manifest v3）

指定したページを適当なcssで上書きする拡張機能が欲しかった。
[これ](https://qiita.com/dhomma/items/bb49c9f6b66ee936ff5c) を拡張機能でやりたいだけなんだけど、なかなかうまく動くのが無いから勉強がてら自分で作ることにした。。。
結構ハマったのでメモ


↓ 開発した拡張機能（指定したウェブサイトにCSSを埋め込む雑アプリ
[css-injector](https://chrome.google.com/webstore/detail/css-injector/bonanlhbgkbmdbpgmljgpkcihiccfbha?hl=ja)

できたコードはこれ
https://github.com/m-masataka/css-injector-extension


## まずは考える
chrome-extensioinでは３つの構成要素があり、それぞれに何をさせるかを考える。
- Content Scripts（ブラウザのイベントをトリガーに動くスクリプト）
    - 設定したcssをページに埋め込む。多分今回のメインロジック
- Browser Action（ポップアップメニューとか）
    - cssの設定と保存を行う
- Event Page（バックグラウンドで動くなにか）
    - 今回はあまり役目無し
    - ブラウザ起動時の表示アイコン制御に使った

↓のページがすごくわかりやすかった。
https://qiita.com/sakaimo/items/5e41d6b2ad8d7ee04b12

### はじめに
とりあえずBrowser ActionはReactで書くのでcreate-appする。
```shell
yarn create react-app sample_app --template typescript
```

### Manifest
一旦必要なmanifestは書いておく。
build後のフォルダ全体が拡張機能のパッケージになるので、このマニフェストはpublicにいれとく。
permissionsで不要なものを定義すると後々審査で怒られるので注意。開発中は必要ないもの入れても大丈夫そう。

```json
{
  "name": "CSS Adopter",
  "manifest_version": 3,
  "version": "0.1",
  "permissions": ["tabs", "activeTab", "scripting", "storage"],
  "options_page": "index.html",
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_icon": "disabled.png",
    "default_popup": "popup.html"
  },
  "content_scripts": [
    {
      "matches": ["https://*/*"],
      "js": ["content.js"],
      "run_at": "document_end"
    }
  ]
}

```

### Browser Action
今回はポップアップと設定ページの２つのページを作るのだけど、Reactでhtml2つ出力するのってどうするんだっけ？
というところでハマった。
作るページはマニフェストにも記載してる↓２つ
- "options_page": "index.html"
- "default_popup": "popup.html"

https://zenn.dev/ulcttku/articles/creating-chrome-extensions-with-react
ここのページ参考にrenderを２つ定義して、buildでゴニョゴニョすることにした。

index.tsx
```react
ReactDOM.render(
  <React.StrictMode>
    <PopupPage />
  </React.StrictMode>,
  document.getElementById("popup") || document.createElement("div")
);

ReactDOM.render(
  <React.StrictMode>
    <OptionsPage />
  </React.StrictMode>,
  document.getElementById("root") || document.createElement("div")
);
```

package.json
```json
    "build": "react-scripts build && npm run rename:popup",
    "rename:popup": "sed 's/root/popup/' build/index.html > build/popup.html",
```
jsを適用するidを変更して２つのページを生成する感じ。


後々さがしたら素晴らしくスマートなサンプルがあった....
https://github.com/chibat/chrome-extension-typescript-starter

適用するcssとサイトの設定はchrome storageに保存する。
使い方は[ここ](https://developer.chrome.com/docs/extensions/reference/storage/)

typescriptでこれを使うためにtype/chromeをインストールする必要がある。
```
yarn add @types/chrome
```
``chrome.storage.(local/sync).set({key: value});`` 
でストレージへ保存できる。
useStateへの反映タイミングと合わせて使う感じで、objectそのまま保存することもできるのでかなり楽ちん。

```typescript
const [css, setCSS] = useState<CSS[]>([]);
setCSS(cs);
if (chrome.storage) {
   chrome.storage.local.set({ css_adopter: cs });
}
```

storageからアイテムを取得するときはこんな感じ。
useEffectで読み出して画面ロード時にデータ反映する。

```typescript
  useEffect(() => {
    if (chrome.storage) {
      chrome.storage.local.get(["css_adopter_enable"], (items) => {
        if (items["css_adopter_enable"]) {
          let changed_enable = items["css_adopter_enable"];
          setEnable(changed_enable);
        }
      });
    }
  }, []);

```

### Content Script
ここでcssの追加を行う。
loadイベントをトリガーにすることで、なんらかページがロードされたときにcssを挟み込むことができる。
```typescript
window.addEventListener("load", () => {
  chrome.storage.local.get(["css_adopter"], function (items) {
    .... CSSの処理

```

で、このcontent_scriptsが実行されるタイミングがmanifestで指定できるらしく、
[run_at](https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/manifest.json/content_scripts#run_at)  = ``document_end``でページが全部ロードされたタイミングとなるらしい。
```json
  "content_scripts": [
    {
      "matches": ["https://*/*"],
      "js": ["content.js"],
      "run_at": "document_end"
    }
  ]
```

### Event Page
最後にbackground script
ここはあまりやることがないけど、拡張機能のアイコンでON/OFFを表現したいときとかに使えそう。
manifest v3でbackgroundの処理はservice workerに移行するらしい。
https://developer.chrome.com/docs/extensions/mv3/intro/mv3-overview/#service-workers
ChromeのService Workerって悪いことするイメージしか無いけど大丈夫なのかな？

ここはほんとにiconを入れ替えるだけ。
chrome.action.setIconでアイコン変えられるみたい。

```typescript
chrome.storage.local.get(["css_adopter_enable"], (items) => {
  if (items["css_adopter_enable"]) {
    let changed_enable = items["css_adopter_enable"];
    chrome.action.setIcon({
      path: (changed_enable ? "enabled" : "disabled") + ".png",
    });
  }
});
```



## できた
こんな感じでcssを指定すると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520924/a73daf1d-9d36-205a-b57d-9bb8bde7cda1.png)

googleが気持ち悪い赤になる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520924/2426eb19-45a5-ce3d-fcd8-e507a9f704d7.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520924/ecae41ac-0740-5f72-384c-a19a82ef3cbb.png)
ポップアップで有効化/無効化ができる。