ブラウザ拡張機能におけるメッセージパッシング


はじめに

ChromeやFirefoxで採用されているWebExtensionsによる拡張機能の開発を前提に話をします。
拡張機能の作り方は思ったより簡単ですし、ストアへの公開時の審査も厳しくないので敷居は低く、でも自分や他人の役に立ちやすいので、おすすめの開発だと思います。
HTMLとCSSとJavaScriptで作れます。
今回はこのブラウザ拡張機能を作るときに使用するメッセージパッシングについてです。

拡張機能の構成について

さて、この拡張機能ですが、作成するときに大きくわけて2つの部分から構成されます。
(1のみとか2のみの拡張機能もありますが、その場合は特に言うこともないので)

  1. 拡張機能自体(background,popup)
  2. Webページに埋め込まれる機能(content_scripts)

拡張機能自体の部分

backgroundのスクリプトはブラウザが起動したときにロードされます。
拡張機能のボタンを押したときに、ユーザーに設定パネルを表示させたり、ユーザーからの入力を受け付けたいときに使用するのがpopupです。これはボタンが押されたときにロードされ、表示されます。

1の拡張機能自体というのはブラウザ自身の機能や情報にアクセスできるものです。ブラウザ自身は現在開かれているタブや、履歴、ブックマークといった情報を持っていて、それに対して専用のAPIを使ってアクセスして、ユーザーに便利な情報や操作を提供したりできます。
例えば独自の管理機能を持ったブックマーク機能なんてものも作れたりするかもしれません(やったことないので)。
というように、純粋にブラウザの機能を拡張(追加)する部分がこれです。
ですので、現在表示中のWebページの内容を取得したり変更したりといったことはできません

2のWebページに埋め込まれる機能というのはウェブページが表示される前(タイミングはいくつかある)に埋め込まれ、記述されたイベントのタイミングで実行されます。例えばページのロードが完了したときとか、ボタンが押されたときとかです。
これを使うことで表示されたWebページのコンテンツに対して様々な操作を行うことができます。
特定のデータを取得したり、追加したり、変更したりといったことです。
Webページのコンテンツに対する操作のみが可能で、1側の構成要素であるボタンとかポップアップを操作することはできません

1と2は完全に別のプログラムで1から2の機能を直接呼び出すことができませんし、逆もまた同様です。
例えば、上の画像のように拡張機能のボタンが押されたタイミングでWebページの特定の単語をハイライトする、という機能を作る場合を考えると、1側である拡張機能ボタンを押したときに実行される処理の中で、2側の機能を指定して呼び出す必要がありますが、1側は2側の機能が見えませんし使えません。ハイライトできません。

どうするの?

ではどうするかというと、メッセージパッシングという方法で、1側と2側で情報をやり取りし、それに従って必要な処理を行うことになっています。
ブラウザを経由して1と2が協調する仕組みです。
ブラウザが提供するSendMessageメソッドでメッセージを送って、受け取る側はOnMessageイベントにハンドラを追加してメッセージを受け取ったときの処理を書きます。
公式の説明はこちら

メッセージパッシングのやり方

以下は1から2へメッセージを送る場合です。
拡張機能のボタンが押されたときに、特定の文字をハイライトしたい、というシナリオですね。
メッセージ送信側は1側、つまりbackgroundやpopupのスクリプト(JavaScript)です。
メッセージ受信側は2側、つまりcontent_scriptsのスクリプトです。

メッセージ送信側(1側)
browser.tabs.sendMessage(メッセージ送信先のタブID, メッセージJSON);
メッセージ受信側(2側)
browser.runtime.onMessage.addListener(function (message) {
//なんか処理
//messageの内容で処理を分けたりする。
//例えば、ハイライトしたい文字列を1側から受け取ったり。
});

では、2側から1側へメッセージを送る場合はどうしたら良いでしょうか。
例えば、ハイライトした単語の個数を拡張機能のボタンにバッジとして表示したい、とかです。
その場合は以下のようにします。

メッセージ送信側(2側)
browser.runtime.sendMessage(メッセージJSON);
メッセージ受信側(1側)
browser.runtime.onMessage.addListener(function (message) {
//なんか処理
//messageの内容で処理を分けたりする。
//例えば、ハイライトしたい文字列を1側から受け取ったり。
});

1→2へのメッセージ送受信とほとんど同じですが、若干異なる箇所があります。
それはメッセージ送信時のsendMessageメソッドの書き方です。
宛先を指定せずメッセージのみを引数として与えています。
2側からのメッセージの送信先は暗黙的にセットとなっている拡張機能となっているのでしょう。
逆に1側からみると送信先はタブの数だけあるので、いずれかを指定しなければならないということなのですね。

拡張機能同士のメッセージパッシングは?

これも同様にできます。
送受信を行うのはそれぞれの拡張機能の1側になります。

やり方はほとんど同じですが、送信メソッドと受信イベントが異なります。
拡張機能1から拡張機能2へメッセージを送る場合を考えてみます。
例えば、拡張機能1がオンになったら拡張機能2はオフにする、というシナリオですね。

メッセージ送信側(拡張機能1の1側)
    browser.runtime.sendMessage(拡張機能2のID, メッセージJSON);
拡張機能2の1側
browser.runtime.onMessageExternal.addListener(function (message, sender) {
    //必要であれば送信元の拡張機能のIDを確認する
    if (sender.id === 拡張機能1のID) {
    //なんか処理
    //例えばボタンをグレーアウトしたり、別の種類のハイライトをしたり。
    }
});

これも基本的には同じです。
ただし、送信側では送信先の拡張機能のIDを指定します。わかりやすいですね。
受信側ではonMessageExternalイベントにハンドラを追加しています。

外部からのメッセージ受信イベントが用意されているのでそれを使うということです。
onMessageイベントとは若干異なり、送信元の情報を引数として受け取るようになっています。
この例ではsenderに送信元の情報が自動的に入ります。

複数の拡張機能と連携するのであれば、送信元の拡張機能をIDで識別して別々の処理を行うことが可能です。

サンプル

githubに2つのFirefox拡張機能を公開しました。
Extension1とExtension2です。
それぞれの拡張機能は以下のようなメッセージパッシングするだけのものです。

Extension1

ダウンロードはこちらから。
Firefoxで「一時的なアドオンを読み込む」でインストールしてください。

  • 拡張機能のボタンを押す(background)とcontent側にメッセージが送られ、それを受け取ったcontent側によってConsoleに「Extension1 content:Message from background.」という文字が出力される。
  • 拡張機能のボタンを押すとExtension2にメッセージが送られ、それを受け取ったExtension2は自身のバッジに表示されるカウントを1インクリメントする。(拡張機能間のメッセージパッシング)
  • 表示されているWebページ上でクリックするとExtension1のバッジのカウントを1インクリメントする。

Extension2

ダウンロードはこちらから。
Firefoxで「一時的なアドオンを読み込む」でインストールしてください。

  • 拡張機能のボタンを押す(background)とcontent側にメッセージが送られ、それを受け取ったcontent側によってConsoleに「Extension2 content:Message from background.」という文字が出力される。
  • 拡張機能のボタンを押すとExtension1にメッセージが送られ、それを受け取ったExtension1は自身のバッジに表示されるカウントを1デクリメントする。(拡張機能間のメッセージパッシング)
  • 表示されているWebページ上でクリックするとExtension2のバッジのカウントを1デクリメントする。

動作はこんな感じ

基本的な動作はこれで試すことができると思います。
見てみるとメッセージパッシング自体は以外とシンプルなことがわかると思います。
Firefox用の拡張機能として作っていますが、manifest.jsonだけをchrome用に若干修正すれば、その他はすべてそのまま使えるはずです。