🔉

拡張機能でユーティリティ関数を共有したい(ポップアップ、バックグラウンド)

2023/11/05に公開

同じロジックを複数箇所で使う場合、utilsのようなディレクトリを作ってそこにコードを書き、必要な箇所でimportするのが良くある手です。これをChrome拡張機能の開発で行う場合について解説します。以下の5箇所で読み込んで実行させてみます。

  • ポップアップ
  • オプション
  • バックグラウンドスクリプト
  • コンテントスクリプト
  • DevTools

最終的なディレクトリ構造は次のようなものになります。

ディレクトリ構造
.
├── background.js
├── content_scripts
│   └── content.js
├── devtools
│   ├── devtools.html
│   ├── devtools.js
│   ├── panel.html
│   └── panel.js
├── manifest.json
├── options
│   ├── options.html
│   └── options.js
├── popup
│   ├── popup.html
│   └── popup.js
└── utils
    └── utils.js

最小の構成で拡張機能を作る

まずは最小限の構成で拡張機能を作ります。以下の3つのファイルを作成して、ブラウザに読み込んでください。ディレクトリ構成は次のようなものになります。

.
├── manifest.json
├── popup
│   ├── popup.html
│   └── popup.js
└── utils
    └── utils.js
manifest.json
{
  "manifest_version": 3,
  "name": "Share Utils",
  "version": "1.0.0",
  "action": {
    "default_popup": "popup/popup.html"
  }
}
popup/popup.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Popup</title>
  </head>
  <body>
    <h1>Popup Page</h1>
    <script src="popup.js"></script>
  </body>
</html>
popup/popup.js
// まだ空

共有したいファイルを作成する

次に共有したいファイルをまとめたJSファイルを作成しましょう。utilsフォルダを作りその中にutils.jsを作成します。実験のため、文字列を返すクラスメソッドと関数を定義しています。このとき、classfunctionの前にexportを書かなくて構いません。このページでは単純なサンプルで説明するために文字列を返すユーティリティー関数を作りました。一方で、chrome APIを使える場所で読み込むならchrome APIを使ったユーティリティ関数を作ることができます。(例えばコンテンツスクリプトでchrome.tabs.*APIは使えない)

utils/utils.js
class Utils {
  getMethod() {
    return "hello from method";
  }
}

function getFunction() {
  return "hello from function";
}

① ポップアップで読み込む

ユーティリティ関数をポップアップで読み込んでみましょう。ポップアップからユーティリティ関数を読み込むには、ライブラリを使わない通常のWeb制作と同様に、scriptタグからJSファイルを読み込みます。拡張機能開発ではコンテンツセキュリティーポリシーにより、JSファイルはscriptタグ経由で読み込む必要があります。

popup/popup.html
  <body>
    <h1>Popup Page</h1>
+   <script src="/utils/utils.js"></script>
    <script src="popup.js"></script>
  </body>

次にユーティリティ関数をpopup.jsで読み込んでみましょう。このとき、importを使わなくても関数を呼び出せることに注意してください。

popup/popup.js
// クラスの呼び出し
+const utils = new Utils();
+const methodMsg = utils.getMethod();
// 関数の呼び出し
+const functionMsg = getFunction();

+alert(`${methodMsg} - ${functionMsg}`);

動作確認をするため拡張機能を再度読み込み、ポップアップをクリックしてください。するとアラートが呼び出され、次のような画面になります。

注意1: 実行される順に書く

htmlファイルは上から下へ順に実行されます。それはscriptタグでも同様です。次のようなhtmlファイルがあれば、まずfirst.jsが実行され、次にsecond.jsが実行されます。

<script src="first.js"></script>
<script src="second.js"></script>

今回のようにユーティリティ関数を別のJSファイルで使いたい場合、まずユーティリティ関数を定義したJSファイルを読み込み、その後呼び出し先のJSファイルを読み込む必要があります。なので、下のようにscriptタグを逆順にするとエラーが起きてダイアローグ画面が表示されません。

popup/popup.html
  <body>
    <h1>Popup Page</h1>
-   <script src="/utils/utils.js"></script> <!-- 呼び出し順を逆にした -->
-   <script src="popup.js"></script>
+   <script src="popup.js"></script>
+   <script src="/utils/utils.js"></script>
  </body>

なぜエラーが出たのかというと、呼び出し元のファイルでまだ読み込みが完了してないクラスが使われたからです。もう一度popup/popup.jsをみてみると、popup/popup.jsの中でutils/utils.jsファイルのUtilsクラスが使われていることがわかります。

content_scripts/content.js
const utils = new Utils();           // 👈
const methodMsg = utils.getMethod(); // 👈  utils/utils.js が読み込まれてることを前提に書いている
const functionMsg = getFunction();   // 👈
alert(`${methodMsg} - ${functionMsg}`);

うまく表示されなくなったのはutils/utils.jsを読み込む前にUtilクラスを使ったことが原因です。サンプルコードではcontent_scripts/content.jsのトップレベルでユーティリティ関数を使いましたが、その時点ではutils/utils.jsはまだ読み込んでいません。
もし仮にユーティリティ関数が何かのイベントリスナーの中にあり、ページが完全に読み込まれた後にUtilsクラスが呼び出されるのであれば問題なく動作します。しかし思わぬエラーを引き起こさないためにもコンテンツスクリプトに依存関係があれば、呼び出される順番に定義するのがいいでしょう。

②オプションページで読み込む

オプションページも同様にscriptタグから読み込みます。ポップアップもオプションページも独立したWebページのようなものなのでJSファイルの読み方も同じです。次のファイルを追記したあと拡張機能を再度読み込みし、ポップアップを右クリックし、「オプション」をクリックすると先ほどと同様にアラートが出現します。
オプションページ関連はルートディレクトリにoptionsディレクトリを作り、その中にファイルを作成していることに注意してください。また、importを使わなくても読み込んだクラスと関数が使えることにも注意してください。

manifest.json
{
  "manifest_version": 3,
  "name": "Share Utils",
  "version": "1.0.0",
  "action": {
    "default_popup": "popup/popup.html"
  },
+ "options_page": "options/options.html"
}
options/options.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Options</title>
  </head>
  <body>
    <h1>Options Page</h1>
    <script src="/utils/utils.js"></script> // 👈① popup.htmlと同じ書き方
    <script src="options.js"></script>
  </body>
</html>
options/options.js
const utils = new Utils();
const methodMsg = utils.getMethod();
const functionMsg = getFunction();
alert(`${methodMsg} - ${functionMsg}`);

注意1: 実行される順に書く

ポップアップで説明したのと同様に、JSファイルの依存関係により呼び出し順を考慮する必要があります。utils/utils.jsの内容がoptions.jsで使われているので上のコードのような順番で書いてます。

補足:ポップアップとオプションページでUIが異なる理由

alert()で呼び出すダイアローグ画面はポップアップとオプションページで異なるものでした。オプションページはWeb開発でalert()を使ったことなら人なら何度も見たことがあるダイアローグ画面でした。一方ポップアップの画面は見慣れないものでした。これらの違いはポップアップを開いてる途中に別の画面をクリックする(フォーカスを失う)ときにポップアップが閉じてしまう事実に関連します。ポップアップで出てきたダイアローグは別ウィンドウとして表示されるので、OKボタンを押してもポップアップは開いたままです。ポップアップからフォーカスを失わないようにするためポップアップのダイアローグ画面は特別なものになってます。

③コンテンツスクリプトで読み込む

コンテンツスクリプトでJSファイルを共有するには、通常のコンテンツスクリプトと同様にmanifest.jsonで宣言する必要があります。ユーティリティ関数を定義したファイルのパスutils/utils.jsjsキーの配列に入れるだけで、コンテンツスクリプトを定義しているcontent_scripts/content.jsファイルでユーティリティ関数を使えるようになります。
下のようにファイルを追記した後、拡張機能を再度読み込み、https://example.com/など任意のサイトに移動してください。このようにページの後ろに「hello from method」と「hello from function」が追記されていれば成功です。

manifest.json
{
  "manifest_version": 3,
  "name": "Share Utils",
  "version": "1.0",
  "action": {
    "default_popup": "popup/popup.html"
  },
  "options_page": "options/options.html",
+ "content_scripts": [
+   {
+     "matches": ["<all_urls>"],
+     "js": ["utils/utils.js", "content_scripts/content.js"] // 👈 注意1
+   }
+ ]
}
content_scripts/content.js
// ユーティリティ関数の読み込み
const utils = new Utils();
const methodMsg = utils.getMethod();
const funcMsg = getFunction();

// Pタグを作る
const p1 = document.createElement('p');
p1.innerText = methodMsg;
const p2 = document.createElement('p');
p2.innerText = funcMsg;

// PタグをWebページに挿入する
document.body.appendChild(p1);
document.body.appendChild(p2);

注意1: 実行される順に書く

コンテンツスクリプトもJSファイルの依存関係に応じて呼び出し順を気をつける必要があります。ポップアップやオプションページはhtmlファイル内のscriptタグにより順番を決めていました。コンテンツスクリプトではmanifest.jsonjsキーのプロパティの順番により呼び出される順番が決まります。

manifest.json
      "js": ["utils/utils.js", "content_scripts/content.js"]

プロパティ内のJSファイルの順番を入れ替えて、次のように書き換えてください。

manifest.json
-     "js": ["utils/utils.js", "content_scripts/content.js"]
+     "js": ["content_scripts/content.js", "utils/utils.js"]

するとサイトに挿入されていた文字列は消えてしまいます。理由は先ほどと同じで、ユーティリティ関数を読み込む前に、ユーティリティ関数を使ったからです。

④バックグラウンドスクリプトで読み込む

バックグラウンドスクリプトからユーティリティ関数を呼び出すにはimportScript関数を使う必要があります。ポップアップは独立したWebページ、コンテンツスクリプトは閲覧してるWebページに挿入されるファイルなので違和感のない書き方でした。しかしバックグラウンドスクリプトの実態はサービスワーカーです。サービスワーカーに外部ファイルを読み込ませたいときはサービスワーカー専用のimportScript関数を使う必要があります。以下のようなコードを書いてみましょう。

manifest.json
{
  "manifest_version": 3,
  "name": "Share Utils",
  "version": "1.0",
  "action": {
    "default_popup": "popup/popup.html"
  },
  "options_page": "options/options.html",
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content_scripts/content.js", "utils/utils.js"]
    }
  ],
+ "background": {
+   "service_worker": "background.js"
+ }
}
background.js
importScripts('utils/utils.js')

setInterval(() => {
  const utils = new Utils();
  const methodMsg = utils.getMethod();
  const functionMsg = getFunction();
  console.log(`${methodMsg} - ${functionMsg}`);
}, 5000)

バックグラウンドスクリプトではalert()が使えないので代わりにconsole.log()を使いました。バックグラウンドスクリプトのDevToolsを開くには、chrome://extensionsを開き、拡張機能の「Service Worker」をクリックし、コンソールパネルを開いてください。「hello from method - hello from function」という文字列が5秒ごとに出力されると成功です。

⑤DevToolsで読み込む

DevToolsでのファイルの共有の仕方はポップアップ、オプションページと同様に1つの独立したWebページのようなものなので記述する順番に気をつけてscriptタグで読み込むだけです。
devtoolsディレクトリの中に、devtools.htmldevtools.jspanel.htmlpanel.jsの4つのファイルを作ってください。その後、次のようにコードを追記してください。

manifest.json
{
  "manifest_version": 3,
  "name": "Share Utils",
  "version": "1.0",
  "action": {
    "default_popup": "popup/popup.html"
  },
  "options_page": "options/options.html",
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content_scripts/content.js", "utils/utils.js"]
    }
  ],
  "background": {
    "service_worker": "background.js"
  },
+ "devtools_page": "devtools/devtools.html"
}

devtools.html
<!DOCTYPE html>
<body>
  <script src="devtools.js"></script>
</body>
</html>
devtools.js
chrome.devtools.panels.create(
  "Shared Panel",
  "",
  "/devtools/panel.html"
)
panel.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>DevTools</title>
  </head>
  <body>
    <h1>DevTools</h1>
    <script src="/utils/utils.js"></script>
    <script src="./panel.js"></script>
  </body>
</html>
panel.js
const utils = new Utils();
const methodMsg = utils.getMethod();
const functionMsg = getFunction();
alert(`${methodMsg} - ${functionMsg}`);

カスタムパネルはわかりにくい場所に配置されています。まずはDevToolsを開いき、「要素」「コンソール」「ソース」などとパネル名が続いている右端にある「>>」をクリックします。するとさらにパネル名が表示され「Shared Panel」をクリックします。するとアラートを示すダイアローグ画面が出てくるはずです。
ちなみに「Shared Panel」というパネル名はdevtools/devtools.jscreate関数の第一引数で定義した名前がそのまま使われているので、用途によって自由に名前を決めることができます。詳しい解説は、DevToolsのサンプルを作るときに解説します。

参考サイト
https://dev.to/paulasantamaria/chrome-extensions-reusing-code-3f1g

Discussion