💨

Chrome拡張機能の開発

2024/03/26に公開

拡張機能の構成

拡張機能には大きく三つの要素が両立する:

  1. background(現在service workerという):DOMにアクセスできないが、その代わり外部サーバーに接続できるので、バックエンドっぽい
  2. popup:拡張機能のアイコンをクリックするときに表示されるポップアップ
  3. contents scripts:全てのブラウザーページで実行される。ブラウザーのランタイム機能を部分的にしか使えない

そして、それぞれの要素のやり取り方法はこちら:

要素間のやり取り図

しかしこの度開発した拡張機能では、contents scriptsへのやりとりが必要とされず、どちらかというと次の図になった:

contents scriptsから他の要素への一方的なコミュニケーション

ポップアップ構築

ポップアップの役割は比較的に単純:ツールバーにある拡張機能アイコンがクリックされたときに表示されるポップアップのになる。そのため、ただのHTMLファイルであり、次のようにマニフェストで指定できる:

manifest.json
"action": {
	"default_popup": "popup.html"
}

あとはポップアップの裏側に作動するpopup.jsをHTML内に<script>タグとともに埋め込む。

background(またはService Worker)

マニフェストv3から名前はbackgroundからservice workerに変わり、動作も少し違う。
Chrome for Developersによると、現在のサービスウワーカーは「拡張機能の中心的なイベント ハンドラ」であり、イベントを長く受信しなければシャットダウンするという特徴がある。

サービスワーカー側でchrome.runtime.onMessage.addListenerでリスナーを実装することでchrome.runtime.sendMessageからのメッセージを処理できるようになる。(参照

よくある問題

Unchecked runtime.lastError: The message port closed before a response was received:このエラーはsendMessageへの不適切な返事により発生する。この場合、解決方法は二つ:

  1. onMessage.addListenerの最後にtrueを返す
  2. ちゃんとしたPromiseを返す

Content Scripts

参照:Chrome - コンテンツ スクリプト

役割:

  1. イベントの処理
  2. DOM操作

例えばGoogle翻訳の場合、文字選択イベントを検知すれば、コンテンツ スクリプトは「翻訳」ボタンを表示し、今回クリックを検知すればボタンを消すという例が挙げられる。

コンテンツ スクリプトもmanifestに指定する必要がある:

manifest.json
"content_scripts": [
    {
        "js": ["content-script.js"],
        "matches": ["<all_urls>"]
    }
]

jsフィールドでスクリプトのパスを指定した上で、matchesにスクリプトの挿入先を指定する。普段はmatches<scheme>://<host>/<path> のようなURLを渡すべきだが、こうすればURLに一致するページでのみcontent-script.jsがロードされる。その反面、全てのページに一致する<all_urls>を指定すると「Chrome ウェブストアでの拡張機能の審査には時間がかかることがあります」。

ボタン表示

間違った方法:chrome.windows.createは完全に新しいウインドウを作成してしまう。

代わりに、新しいdivを作り、背景画像にAnkiのアイコンを指定する。また、ユーザが文字を選択してマウスボタンを放した位置に表示する。

画像の指定

背景画像なので、画像へのパスを.style.backgroundImageに代入したいが、まずはmanifestのweb_accessible_resourcesに画像へのパスを追加する(assetsフォルダなら"assets/*")。

次にchrome.runtime.getURLで正しいパスをCSSのurl関数に渡してから代入できる。

位置

正しい位置に表示するために二つの要素を揃える:

  1. マウスイベントのclientX/clientY:複数の画面に対応する。screenX/Yだと、どうしてもマウスの位置はメイン画面を元に計算されるため、間違った座標になってしまう。
  2. clientX/YscrollingElementの高さ・幅を加える。でないと、スクロールして文字を選択してもボタンはスクロールした分間違った位置に生成される。

セットアップ

ECMAScriptモジュールへの変換

import構文を使いたい場合は次のファイルにファイルのタイプはmoduleであることを示さないといけない:

  • manifest.jsonにbackgroundのsw.jsをmodule化:

    manifest.json
    "background": {
        "service_worker": "sw.js",
        "type": "module"
    }
    

    ただし、上記はChromeの場合のみの機能する。詳細はmanifestの違い

  • popup.htmlで<script>のtypeをmoduleにする

  • package.jsonに"type": "module"

ChromeとFirefoxの違い

参照:Build a cross-browser extension

厳密に言えば、Chrome対Firefoxというよりも、FirefoxとSafari対Chrome、OperaとEdgeのは現状である。

例えば、Firefoxならchrome.runtimeではなく、browser.runtimeになるわけだ。

manifestの違い

  1. id: Firefoxではマニフェストv3の拡張機能のidを準備する必要がある
"browser_specific_settings": {
  "gecko": {
    "id": "extension@example.com"
  }
}
  1. background: Chromeでは"service_worker"、Firefoxでは"page"か"scripts"("scripts"なら複数のファイルを指定できる)

Chrome:

"background": {
  "service_worker": "background.js",
  "type": "module"
}

Firefox:

"background": {
  "page": "background.js"
  // か
  "scripts": ["helper.js", "background.js"],
  "type": "module"
}

browser-polyfillで互換性を高める

browser.*を使って不具合が発生しないように、WebExtension browser API Polyfillを使うといい。

Discussion