Chrome拡張の開発環境をWebpack+TypeScriptで構築する

目次
  1. 動作環境
  2. GitHub
  3. ファイル構成
  4. 開発環境の準備
  5. docker-compose.yml
  6. package.json
  7. tsconfig.json
  8. webpack.config.js
  9. Chrome拡張の開発
  10. manifest.json
  11. tsファイル
  12. cssファイル
  13. アイコン画像
  14. Chromeで動作確認する
  15. ビルド
  16. 拡張機能の追加
  17. ソースコードを変更した時
  18. 標準出力が見たい時
  19. 参考

このWebサイトのココをちょっと直したい…そんな思いからChrome拡張の開発を始めました。 ただやっぱりJavaScriptよりTypeScriptが書きたいと思い、環境を構築してみました。

動作環境

  • OS: Windows 11 Home 21H2
$ docker --version
Docker version 20.10.14, build a224086

$ docker-compose --version
docker-compose version 1.29.2, build 5becea4c

GitHub

https://github.com/TansanMilMil/chrome-extention-template

当記事で紹介する開発環境のテンプレートを公開してます。ご自由にお使いください!

ファイル構成

今回、最終的に下記のファイル構成になるよう作成をしていきます。

$ tree
.
|-- docker-compose.yml
|-- node_modules
|-- package-lock.json
|-- package.json
|-- public
|   |-- image
|   |   `-- icon.png
|   |-- manifest.json
|   `-- style
|       `-- styles.css
|-- src
|   |-- background.ts
|   `-- main.ts
|-- tsconfig.json
`-- webpack.config.js

webpack、TypeScriptを使いたいので設定ファイルとしてwebpack.config.js、tsconfig.jsonを用意します。また、npm startでwebpackによるビルド、ファイル変更監視を実現できるよう、package.jsonにも一部追記をします。

Dockerコンテナ内でNode.jsを動かす構成にしていますが、必須ではないのでお好みでどうぞ。

開発環境の準備

docker-compose.yml

下記のdocker-compose.ymlを作成し、コンテナ内でNode.jsを動作させるようにします。

version: '3'
services:
  node:
    image: node:18.2
    working_dir: /chrome_extention
    volumes:
    - .:/chrome_extention
    tty: true

package.json

package.jsonを作成し、開発に必要なパッケージをdevDependenciesに記述しておきます。

  • Chromeの型定義
    • “@types/chrome”
  • Webpack用
    • “copy-webpack-plugin”
    • “webpack”
    • “webpack-cli”
  • TypeSciprt用
    • “ts-loader”
    • “typescript”
{
  "name": "chrome extention",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "webpack --watch --mode=development",
    "build": "webpack --mode=production",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/chrome": "^0.0.188",
    "copy-webpack-plugin": "^11.0.0",
    "ts-loader": "^9.3.0",
    "typescript": "^4.6.4",
    "webpack": "^5.72.1",
    "webpack-cli": "^4.9.2"
  }
}

記述できたら、docker-compose up -dでDockerコンテナを立ち上げ、docker-compose exec node bashでコンテナに入ってからnpm installします。

scriptsにはnpmコマンドでwebpackを動作させられるよう以下を記述しました。

  • "start": "webpack --watch --mode=development"
    • --watchを付与することでwebpackでビルド後にファイル変更があれば再ビルドさせます。デバッグ時にいちいちビルドのコマンドを叩くのが辛いので。
  • "build": "webpack --mode=production"
    • webpackでビルドする。本番リリース用。

tsconfig.json

TypeScriptの設定ファイルであるtsconfig.jsonを追加します。

{
    "compilerOptions": {
      "target": "es6",
      "module": "commonjs",
      "strict": true,
      "rootDir": "src",
      "esModuleInterop": true,
      "typeRoots": [ "node_modules/@types"]
    },
    "exclude": [
      "node_modules"
    ]
}

webpack.config.js

webpack.config.jsを作成し、webpackの各種設定を記述します。

const path = require("path");
const CopyPlugin = require("copy-webpack-plugin");

module.exports = {
  mode: process.env.NODE_ENV || "development",
  entry: {
    background: path.join(__dirname, "src/background.ts"),
    main: path.join(__dirname, "src/main.ts"),
  },
  output: {
    path: path.join(__dirname, "dist/js"),
    filename: "[name].js",
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: [".ts", ".js"],
  },
  plugins: [
    new CopyPlugin({
      patterns: [
        { 
          from: ".", 
          to: "../",
          context: "public"
        }
      ]
    })
  ],
  devtool: 'cheap-module-source-map',
  cache: true,
  watchOptions:{
    poll: true,
  }
};

entryにトランスパイル対象のtsファイルを記述し、outputにトランスパイルされたjsファイルのパスとファイル名を指定しています。

ビルド後のdistディレクトリ内にtsファイル以外のファイルを含めたいので、pluginsにnew CopyPlugin(...)を記述しています。これでpublicディレクトリの中身がdistにコピーされます。

devtool: 'cheap-module-source-map'で、トランスパイルされるjsのソースマッピングの方式を指定しています。 何も指定しない場合、トランスパイルされたjsにeval()が含まれてしまい、Chromeのデバッグ時に下記エラーが発生し正しく動作しません。

Uncaught EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self' blob: filesystem: chrome-extension-resource:".

watchOptions.pollをtrueに指定しているのは、WSLでDockerコンテナを動作させたときにwebpackのファイル監視がうまく動作しなかった為です。

https://webpack.js.org/configuration/watch/#watchoptionspoll

Chrome拡張の開発

ここまででTypeScriptとWebpackを動作させる環境は用意できたので、あとはChrome拡張に関する設定(manifest.json)とビジネスロジックを書いたtsファイル、css、アイコン画像等を用意します。 なお、ts, css, アイコン画像についてはここでは詳細を解説しません。

ファイル構成に書いたようにsrcディレクトリ以下にtsファイル、publicディレクトリ以下にアイコン画像、css、manifest.jsonを配置します。

manifest.json

manifest.jsonを作成し、Chrome拡張に関する設定を定義します。ここで記述している以外にもパラメータがあり、実装内容によって適宜変えていく必要があります。 実はまだこの部分は勉強中です…😓

なお、manifest.jsonにはバージョンがあり、V2とV3で大きく仕様の変化があったようです。以下は投稿時点での最新であるV3の記述方法であることにご注意ください。

{
    "name": "My Chrome Extention",
    "description": "this is my extention.",
    "version": "0.1",
    "manifest_version": 3,
    "icons": {
        "128": "image/icon.png"
    },  
    "permissions": [ 
        "activeTab", 
        "scripting", 
        "storage",
        "declarativeNetRequest",
        "declarativeNetRequestFeedback",
        "cookies"
    ],
    "action": {
      "default_icon": "image/icon.png",
      "default_title": "my-chrome-extention",
      "default_popup": "popup.html"
    },
    "background": {
        "service_worker": "js/background.js"
    },
    "content_scripts": [{
        "matches": [ "https://hogefuga.com/*" ],
        "js": [ "js/main.js" ],
        "css": [ "style/styles.css" ],
        "run_at": "document_end"
    }]
}

permissionsでは開発するChrome拡張で許可する機能を記述します。

  • 例えばCookie関連のAPIであるchrome.cookieを使用したい場合にはpermissionsに"cookies"を追加しなければエラーにより動作しません。
  • API毎に指定すべきpermissionsは、下記ページに列挙されているAPIの詳細ページにて確認できます。

iconsでchrome://extensionsに表示されるアイコン画像のパスを指定しています。

actionでChromeツールバーの拡張機能アイコンの設定を指定しています。

  • action.default_iconで拡張機能アイコンの画像パスを指定しています。アドレスバーの右横に表示されるアイコンのことですね。
  • action.default_titleは拡張機能アイコンにマウスを乗せた際に表示されるツールチップの文字列です。
  • action.default_popupは拡張機能アイコンをクリックした際に表示するhtmlです。

content_scriptsで特定のURLを開いた際に適用するjs, cssを定義しています。

  • matchesには適用条件となるURLを指定します。URLが一致した場合にjs, cssが適用されます。アスタリスクはワイルドカードとして使用可能です。
  • jsは適用するjsファイル、cssは適用するcssファイルです。
  • run_atは適用タイミングで、document_endは「DOMが構築され、画像やフレームなどのサブリソースがロードされる前のタイミングで適用する」ことを指します。他にも種類があるので詳細は下記を参照ください。

background.service_workerではバックグラウンドで動作させたいjsファイルを指定しています。今回はトランスパイル後のjsファイルのことを指します。 Chrome拡張ではservice_worker上でしか動作しないコードが存在する為、これを使わざるを得ないケースがあります。

tsファイル

実装したい要件に沿ってガリガリコードを書きます。

Chrome拡張では専用のAPIが用意されていますので、下記APIリファレンス等も参考になると思います。

https://developer.chrome.com/docs/extensions/reference/

cssファイル

表示ページに適用するスタイルをガリガリ書きます。

既にページ側で定義されているCSSプロパティをChrome拡張側で上書きしたい場合には、!importantを付与して適用する優先順位を変えてあげる必要があります。

アイコン画像

良い感じのアイコンを用意してあげましょう!

manifest.jsonではpngを指定しましたが、svgでもOKなようです。サイズ別に指定も可能です。詳細は以下をご覧ください。

https://developer.chrome.com/docs/extensions/mv2/manifest/icons/

https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_action#syntax

Chromeで動作確認する

ビルド

Dockerコンテナ内でnpm startまたはnpm run buildしましょう。 distディレクトリ内に各種ファイルが出力されるはずです。

さくっとデバッグしたい時はファイル変更時の自動ビルドが効くnpm startがおすすめです。

拡張機能の追加

Chromeでアドレスバーにchrome://extensionsを入れ、拡張機能の画面を表示します。

「デベロッパーモード」をONにし、「パッケージ化されていない拡張機能を読み込む」を選択してdistディレクトリを選択します。

自分の作った拡張機能がリストに追加されればあとは実際にWebページを開いて動作確認するだけです。

ソースコードを変更した時

ソースコード変更時のdistディレクトリへのビルドはwebpackがやってくれますが、変更されたdistディレクトリのChromeへの適用は手動でやる必要があります。

ビルド完了後、chrome://extensionsで更新ボタンをクリックすると、最新のdistディレクトリがロードされます。ちょっと面倒ですが…🤤

※スクリーンショットの拡張機能はサンプルです。余談ですがこれはYouTube Liveで画面上にコメントが流れるという便利な機能で個人的におすすめです。 https://github.com/fiahfy/youtube-live-chat-flow

標準出力が見たい時

manifest.jsonでcontent_scriptsに指定したjsファイルの標準出力は、通常の開発者ツールのConsoleで確認可能です。

manifest.jsonでbackground.service_workerに指定したjsファイルの場合は少し特殊で、chrome://extensionsの「Service Worker」というリンクをクリックすることでService Worker用のConsoleを確認できます。

また、拡張機能読み込み時のエラーが発生した場合は、「エラー」ボタンが表示されるのでクリックして確認が可能です。

参考

https://developer.chrome.com/docs/extensions/

https://blog.chick-p.work/chrome-extensions-typescript/

https://stackoverflow.com/questions/48047150/chrome-extension-compiled-by-webpack-throws-unsafe-eval-error