Create React Appで始めるChrome拡張

[テックタッチアドベントカレンダー](https://qiita.com/advent-calendar/2019/techtouch)12日目を担当する@takakobemです。
11日目は @taisa831 による [Goのtestingパッケージの基本を理解する](https://qiita.com/taisa831/items/85fea8d970bcadd796b9) でした。
JavascriptではBenchmarkのようなパフォーマンスをテストするものがない（したことがないだけかもしれない）のでいいなぁと思いました。

# はじめに
今回はFacebook公式である[create-react-app](https://create-react-app.dev/)を使ってChrome拡張を作ってみたいと思います。
Chrome拡張といっても、機能は大きく分けて3つあります。

* ポップアップ
* コンテントスクリプト
* バックグラウンドスクリプト

**今回はこれらを全て作っていきます！**

今回実装したものは https://github.com/takakobem/react-chrome-extension-boilerplate にコミットしてあります。

# create-react-appとは
create-react-appは、Reactアプリのビルド環境や開発環境をコマンド一つで用意してくれ、Reactアプリを簡単に作ることができるCLIツールです。
ちなみに本家には以下のように書いてあります。

* **少ない学習コスト**
学習や設定が不要なビルドツールです。即時リロードは開発に集中するのを助けます。デプロイする時は、バンドルが自動的に最適化されます。

* **単一の依存関係**
アプリに必要なビルド依存関係は1つだけです。 create-react-appは基礎となるすべての部分がシームレスに動作することがテストされています。複雑なバージョンの不一致はありません。

* **ロックインなし**
内部では、Webpack、Babel、ESLint、およびその他の素晴らしいプロジェクトを使用してアプリを強化しています。高度な設定が必要な場合は、create-react-appから「イジェクト」し、その設定ファイルを直接編集できます。


create-react-app以外にも、有名なビルドツール（というかフレームワーク）としては[Next.js](https://nextjs.org/)や[GatsbyJS](https://www.gatsbyjs.org/)があります。
どちらも非常に優秀なのですが、これらはウェブサイトに特化されたものになっています。
今回はChrome拡張を作るので、create-react-appを使っています。

# プロジェクトの作成
ではcreate-react-appを使ってChrome拡張を作っていきましょう。
**create-react-appのバージョンは3.3.0を利用しています。
これ以外のバージョンではうまく動かない可能性が非常に高いので気をつけてください。**

以下のコマンドを実行して、プロジェクトを作成します。

```
npx create-react-app@3.3.0 react-chrome-extension
```

これでReactが動く環境ができました。
`yarn start`をするとブラウザで起動すると思います。
本当に簡単にReactアプリが作れちゃうんですよね！
ただ、今回はウェブサイトではなくChrome拡張なので、いろいろとカスタマイズしていきます。

# ポップアップの作成
ポップアップは、右上に表示される拡張ボタンです。
ポップアップはウェブページと同じく、HTMLを読み込むことができます。
構成としてはウェブページと同じなので、ほとんど設定いらずで作れてしまいます。
`public/manifest.json`を以下のように書き換えてみましょう。

```json
{
  "manifest_version": 2,
  "name": "React Chrome Extension",
  "version": "1",
  "browser_action": {
    "default_icon": "logo192.png",
    "default_popup": "index.html"
  }
}
```

これで`yarn build`し`build`フォルダをChromeで読み込むと、右上にReactのマークが出てくると思います。
自前の拡張の読み込み方法は https://naokixtechnology.net/javascript/2851 が参考になるかと思います。

これを押すと、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200751/b0665bda-4e78-595b-5f1d-bff49c3e6f98.png)
のようになっていればOKです！
…なんかへんですよね。
これはcreate-react-appがデフォルトでランタイムスクリプトを埋め込んでいるのが原因です。
以下のように`package.json`のビルドコマンドを修正してやれば良いです。

```diff
-    "build": "react-scripts build",
+    "build": "INLINE_RUNTIME_CHUNK=false react-scripts build"
```

これで再度ビルドし読み込むと、以下のようにちゃんと表示されるはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200751/9e8efbfc-9a77-5334-82e8-9b380d1d6de2.png)

あっという間ですね！

## コンテントスクリプトの作成

次はコンテントスクリプトです。
コンテントスクリプトとは、第三者ページ上でスクリプトを実行することができるものです。
例えば第三者ページの上からUIを表示したい場合などに使えます。

## create-react-appの問題
crete-react-appは一つのエントリーポイントしか持てません。
つまり、このままだとポップアップのみしかビルドできないことになってしまいます。
コンテントスクリプトやバックグラウンドスクリプトをビルドするには、create-react-appが内部で使っているwebpackの設定を変えてやる必要があります。
これを実現するには、詳細は省きますが

1. [patch-package](https://github.com/ds300/patch-package)を使って`node_modules`を直接書き換えてしまう方法
2. イジェクトする方法
3. [react-app-rewired](https://github.com/timarney/react-app-rewired)を使う方法

などがありますが、今回は今後できるだけ最新のcreate-react-appに対応できるようにするために、3の方法を説明したいと思います。

## react-app-rewired
[react-app-rewired](https://github.com/timarney/react-app-rewired)は、**create-react-appの設定の上書きを可能にするもの**です。
これを使うことで、ビルド対象を増やしたりビルド方法を変更したりすることができるようになります。
ただ、公式ライブラリではないので、create-react-appのアップデート次第では使えなくなる可能性や、上書きしていた設定ファイルが適用できなくなる可能性があるので注意してください。
また、[customize-cra](https://github.com/arackaf/customize-cra)も併せて使っていきます。
これはreact-app-rewiredのためにあるようなもので、既存のcreate-react-appの設定を簡単に書き換えられるものです。

## react-app-rewiredとcustomize-craの導入

必要なpackageをインストールします。

```bash
yarn add react-app-rewired customize-cra
```

次に、ビルドコマンドを変更し、react-app-rewiredを使うようにします。

```diff
-  "build": "INLINE_RUNTIME_CHUNK=false react-scripts build",
+  "build": "react-app-rewired build",
```
`INLINE_RUNTIME_CHUNK=false`相当のことはreact-app-rewired内でやってしまうので不要になります。

## コンテントスクリプトを読み込めるようにする

manifest.jsonに、コンテントスクリプトを読むための設定を追加します。
matchesはどこのサイトでも実行されるようにしています。実行ページを限定したい場合は適宜変更してください。

```javascript
  "content_scripts" : [
    {
      "matches": [ "<all_urls>" ],
      "js": ["/static/js/content_script.js"]
    }
  ]
```

## コンテントスクリプトを生成できるようにする

次に、config-overrides.jsを以下の内容で作ります。
後方互換とか気にせず無理やり書き換えているのであしからず！

```javascript
var path = require("path");
const { override, disableChunk } = require("customize-cra");

module.exports = {
  webpack: function(config, env) {
    // ビルドするファイルを追加
    config.entry = {
      main: [path.resolve("src/index")],
      content_script: [path.resolve("src/content_script")]
    };

    // ファイル名にhashが含まれないようにする
    config.output.filename = "static/js/[name].js";

    // 画像ファイルは必ずJSに含めるようにし、svgもその対象にする
    config.module.rules[2].oneOf[0].options.limit = true;
    config.module.rules[2].oneOf[0].test.push(/\.svg$/);

    // mini-css-extract-pluginの設定にstyle-loaderを上書きすることでcssをjsに含める
    config.module.rules[2].oneOf[3].use[0] = {
      loader: require.resolve("style-loader")
    };

    return override(
      // chunk化しない
      disableChunk()
    )(config, env);
  }
};
```

いろいろやっていますが、端的に言うと
「JS、CSS、リソースファイルをまとめて一つの`content_script.js`というファイルにバンドルする」
ことをやっています。

基本的にChrome拡張は読み込むファイルを`manifest.json`で指定してやらないといけません。
デフォルトではファイル名がハッシュ化されていたりファイルが分割されていたりするので、ビルド結果が変わる度に`manifest.json`を変えるのは手間がかかります。
なので、JSもCSSも画像ファイルも全て含めて一つのJSにバンドルしてしまおう」という発想です。
Chrome拡張の各種ファイルはネットワーク経由ではなくローカルから読み込むことになるので、読み込みにかかる時間はほとんど気にしなくて良いという前提でやっています。ただし、ローカルから読み込む場合であっても、ファイルを分離することで起動時のeval時間を削減でき、起動速度を早めることはできると思うので、そこまでこだわる人はファイルを分割したほうがいいでしょう。ハッシュ化やファイル分割がされていても、スクリプトで自動で`manifest.json`を書き換えてあげればいけるはずです。

## コンテントスクリプトの実装
最後に、content_script.jsを作成します。
基本的にはデフォルトのポップアップのものと一緒ですが、ポップアップと違いコンテントスクリプトは自前のHTMLを持っていないため、Reactの描画先の`root`を自前で用意してやる必要があります。
今回は、どこのサイトを開いても画面全体にReactのサンプル画面が出るようにしてみました。

```javascript
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

const root = document.createElement('div');
root.style = "position: absolute; top: 0; bottom: 0; left: 0; right: 0; z-index: 1000000";
document.body.appendChild(root);
ReactDOM.render(<App />, root);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
```

これで再ビルドして拡張を再読み込みし、googleのページを開くと以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200751/d30d5ee0-ff8d-49fe-d95a-3c167b6d655e.png)

ReactのUIで上書きできていることがわかると思います。
Googleの画面が見えなくなってしまいます。
これはもはやブラクラですねｗ

# バックグラウンドスクリプトの実装
コンテントスクリプトができてしまえば、バックグラウンドスクリプトは簡単です。
同じようにやっていきましょう。

## バックグラウンドスクリプトを読み込めるようにする

manifest.jsonに以下を追加します。

```diff
+   "background": {
+     "scripts": ["/static/js/background_script.js"]
+   }
```

## バックグラウンドスクリプトを生成できるようにする

`config.entry`に以下を追加します。

```diff
+      background_script: [path.resolve("src/background_script")]
```

## バックグラウンドスクリプトを実装する

`background_script.js`というファイルを作成します。
今回は、1秒おきにログを出すだけの簡単なものに留めておきます。

```javascript
let count = 0;
setInterval(() => console.log("count = ", ++count), 1000);
```

では再度ビルドして読み込んでみましょう。
拡張のところに、`Inspect views background page`のようなものが出ていたら成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/200751/3fda086b-bacf-bc57-622e-d9484477ac26.png)
`background page`をクリックするとデバッグウインドウが立ち上がり、ログが出ていればOKです。

# おわりに
今回はcreate-react-appを使ってChrome拡張を作る方法を説明しました。
最低限動かせるようにするところまでしか説明できませんでしたので、実際に開発をする際はこれをベースにいろいろと工夫して、開発環境を充実していってもらえればと思います。

明日は @oyoshikeita による「言葉を使ってプロダクトに生命を吹き込む」です。
今までずっとエンジニアの記事でしたが、ついにデザイナーの記事になります！