Chrome拡張機能を React x TypeScript で作るための環境構築した

Webpackを使ってReactとTypeScriptをつかったChrome拡張機能の環境を作ってみたのでシェア。

webpackのバージョンは５系を使用。manifest.jsonのバージョンは3系。

## 成果物
なんの変哲もないどこにでもありふれたカウントアップの拡張機能です。これをReact(TS)からコンパイルして生成しています。

![countup.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651103/08653f9d-0d15-496b-63c1-5e8fc3c45742.gif)

GitHubリポジトリ： https://github.com/yoshisansan/chrome-extension-react-ts-boiler

## ファイル構成

```
src
 assets
  earth-32px.png
  earth-48px.png
 background
  background.ts
 components
  popup
   PopUp.tsx
 styles
  popup.css
 App.tsx
 popup.html
manifest.json
package.json
tsconfig.json
webpack.config.ts
```

今回は
webpack.config.ts
manifest.json
PopUp.tsx
background.ts
の中身だけを解説します。

他は <a href="https://github.com/yoshisansan/chrome-extension-react-ts-boiler">リポジトリ</a> をみてもらえれば。

ビルドを生成すると ChromeExtensionFile という名前で拡張機能のファイルが生成されます。


### webpack.config.tsについて

```webpack.config.ts
import * as path from 'path'
import HtmlWebPackPlugin from 'html-webpack-plugin'
import CopyPlugin from 'copy-webpack-plugin'
import { Configuration as WebpackConfiguration } from "webpack"
import { Configuration as WebpackDevServerConfiguration } from "webpack-dev-server"

interface Configuration extends WebpackConfiguration {
  devServer?: WebpackDevServerConfiguration;
}

const config: Configuration = {
  mode: "development",
  entry: {
    popup: `${__dirname}/src/App.tsx`,
    background: `${__dirname}/src/background/background.ts`,
  },
  devtool: "inline-source-map",
  module: {
    rules: [
      {
        test: /\.(tsx|ts)?$/,
        use: {
          loader: "ts-loader",
          options: {
            configFile: path.resolve(__dirname, 'tsconfig.json'),
          },
        },
        exclude: /node_modules/,
      },
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
            loader: 'babel-loader'
        }
      },
      {
        test: /\.css$/,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: { url: false }
          }
        ]
      },
      {
        test: /\.(png|jpe?g|gif)$/i,
        use: [
          {
            loader: 'file-loader',
          },
        ],
      },
    ],
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js", ".jsx"],
    alias: {
      "components*": path.resolve(__dirname, "src/components*"),
    },
  },
  output: {
    filename: "[name].js",
    path: `${__dirname}/ChromeExtensionFile`,
  },
  plugins: [
    new HtmlWebPackPlugin({
      template: '/src/popup.html',
    }),
    new CopyPlugin({
      patterns: [
        { from: "./src/assets/earth-48px.png", to: "./src/assets/" },
        { from: "./src/assets/earth-32px.png", to: "./src/assets/" },
        { from: "./manifest.json", to: "./" }
      ]
    })
  ],
  target: ["web", "es5"],
}

export default config;
```

entryポイントを複数作ってentrポイントごとにファイルを生成してChrome拡張機能のファイルに合わせました。

```webpack.config.ts
  entry: {
    popup: `${__dirname}/src/App.tsx`,
    background: `${__dirname}/src/background/background.ts`,
  },
省略
  output: {
    filename: "[name].js",
    path: `${__dirname}/ChromeExtensionFile`,
  },
```

コンパイルしたJSファイルと一緒になったHTMLファイルを出力したいので HtmlWebPackPlugin をつかって HTML もコンパイルします。（index.htmlという名前でファイルが生成される）

```webpack.config.ts
  plugins: [
    new HtmlWebPackPlugin({
      template: '/src/popup.html',
    }),
    new CopyPlugin({
      patterns: [
        { from: "./src/assets/earth-48px.png", to: "./src/assets/" },
        { from: "./src/assets/earth-32px.png", to: "./src/assets/" },
        { from: "./manifest.json", to: "./" }
      ]
    })
  ],
```


### manifest.jsonについて

```manifest.json
{
  "name": "Hello World",
  "description" : "Chrome拡張機能のテストアプリ",
  "version": "1.0.1",
  "manifest_version": 3,
  "background": {
    "service_worker": "/background.js"
  },
  "permissions": ["storage", "activeTab", "scripting"],
  "action": {
    "default_popup": "/index.html",
    "default_icon": {
      "32": "/src/assets/earth-32px.png",
      "48": "/src/assets/earth-48px.png"
    }
  },
  "icons": {
    "32": "/src/assets/earth-32px.png",
    "48": "/src/assets/earth-48px.png"
  }
}
```

backgroundファイルもpopupファイルもビルド後のファイル構成に合わせたファイル名で設定しておきました。

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

```manifest.json
  "action": {
    "default_popup": "/index.html",
    "default_icon": {
      "32": "/src/assets/earth-32px.png",
      "48": "/src/assets/earth-48px.png"
    }
  },
```

### components/popup/PopUp.tsxについて

```PopUp.tsx
import React, {VFC, useState} from 'react'

const PopUp: VFC = () => {
  const [plus, setPlus] = useState<number>(0);
  return (
    <div>
      <h1>ReactTS Boilerplate</h1>
      <h2>カウンター</h2>
      <button onClick={() => setPlus(plus + 1)}>{plus}</button>
    </div>
  )
}

export default PopUp;
```
特に珍しくもないuseStateをつかったカウントアップです。これをApp.tsxで読み込んでレンダリングさせています。

### background.tsについて

```background.ts
chrome.runtime.onInstalled.addListener(async () => {
  const url = chrome.runtime.getURL("index.html");
  const tab = await chrome.tabs.create({ url });
  console.log(`タブのID：${tab.id}`);
});
```
background.tsの動作確認として拡張機能をアップロードしたら新しいタブが開かれて index.html ファイルが読み込まれるようにしています。 

```
yarn add -D @types/chrome
```

によりchromeオブジェクトの型ファイルもインストールしています。

### ビルドしてみよう

```
yarn build
```

することによって ChromeExtensionFile が生成されました。

![スクリーンショット 2021-11-10 14.49.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651103/4bd83fb5-34c0-024a-fd74-441ae6ff60c2.png)


## ChromeExtensionをChrome拡張機能ページでアップロード

![スクリーンショット 2021-11-10 14.48.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651103/ff112bdb-8fba-fe2f-4b9c-241f68b77c0f.png)

アップロードすると background 側のスプリクトでインストール直後にタブを開いて index.html を表示させるようにしています。

![スクリーンショット 2021-11-10 14.54.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651103/00176f08-b79c-783a-d934-fe7dd9e3a8b9.png)

GitHubリポジトリ： https://github.com/yoshisansan/chrome-extension-react-ts-boiler