メッセージ

コンテント・スクリプトは拡張機能ではなくウェブページのコンテキスト上で実行されるため、拡張機能側との通信手段が必要になることがしばしば発生する。例えばRSSリーダーを拡張機能で作ろうとした場合、コンテント・スクリプトはページがRSSを提供しているか確認し、ページの有無によってアイコンの表示を変えるためにバックグラウンドページに通知する。

このような拡張機能とコンテント・スクリプトの通信は、メッセージ機能により実現される。両者はお互いにメッセージを送信し、同じチャンネルで応答することも出来る。メッセージにはJSONを用いる。APIには一度だけメッセージを送るシンプルなものも、永続的に接続する複雑なものも用意されている。この機能を使えば、IDを指定することで他の拡張機能と通信することも可能になる。

単発通信

拡張機能の他の部分に単発の通信を送りたい(そして場合によっては応答を受け取りたい)のであれば、シンプルな extension.sendRequesttabs.sendRequest を用いるのが良い。これにより、JSONでシリアライズされたメッセージを単発でコンテント・スクリプトから拡張機能(或いはその逆)に送ることが出来る。必要であれば、コールバック関数をつけることで応答を受け取ることも可能。

メッセージの送信は、次のようになる。

/* content script */
chrome.extension.sendRequest({greeting: "hello"}, function(response) {
  console.log(response.farewell);
});

拡張機能からコンテントスクリプトへの送信も、非常に似たものになる。異なる点は、どのタブにメッセージを送るのかを指定する必要が出てくる点。次の例では、タブIDを省略することで現在選択されているタブへメッセージを送信している。

/* background.html */
chrome.tabs.getSelected(null, function(tab) {
  chrome.tabs.sendRequest(tab.id, {greeting: "hello"}, function(response) {
    console.log(response.farewell);
  });
});

受信側は extension.onRequest を使ってメッセージの受信イベントを用意する必要がある。これはコンテント・スクリプトでも拡張機能でも同様のコードになる。リクエストは受信側が sendResponse を呼び出すまで開いたままになるので、応答の必要が無い場合でも空の sendResponse を送ることでリクエストを終了させてやることが推奨される。

chrome.extension.onRequest.addListener(
  function(request, sender, sendResponse) {
    console.log(sender.tab ?
                "from a content script:" + sender.tab.url :
                "from the extension");
    if (request.greeting == "hello")
      sendResponse({farewell: "goodbye"});
    else
      sendResponse({}); // snub them.
  }
);

永続通信

単発ではなく永続的な通信を行った方が便利な場合、コンテント・スクリプトから拡張機能(もしくはその逆)へ extension.connecttabs.connect を使って持続的なチャンネルを開く。このチャンネルは必要に応じて名前をつけることが出来、複数の接続を使い分けることも出来る。

利用例のひとつとして、自動的にフォームを埋める拡張機能を挙げてみる。コンテント・スクリプトは拡張機能に対して、とあるログインフォームのためのチャンネルを開き、ページ上の各input要素を送る。拡張機能は受け取った要素に値を埋めて、送り返す。共有化された接続により、拡張機能はコンテント・スクリプトから届く複数のメッセージを共有し続けることが出来る。

接続が開始されると、送受信側共に接続に使うPortオブジェクトを受け取る。

以下に、コンテント・スクリプトがチャンネルを開き、メッセージの送受信を行う方法を例示する。

/* content script */
var port = chrome.extension.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
  if (msg.question == "Who's there?")
    port.postMessage({answer: "Madame"});
  else if (msg.question == "Madame who?")
    port.postMessage({answer: "Madame... Bovary");
});

逆に拡張機能からコンテント・スクリプトへリクエストを送る場合も、接続先のタブをIDで指定してやる必要があることを除けば、ほぼ同様の作業となる。上の例にある connect メソッド部分を、次のように変えればよい。

/* background.html */
var port = chrome.tabs.connect(tabId, {name: "knockknock"});

接続を受け付ける側は extension.onConnect イベントにリスナーを登録する必要がある。こちらについては、コンテント・スクリプトでも拡張機能でも、同じコードになる。拡張機能の別の箇所から connect により接続されると、このイベントが呼び出される。引数に Port オブジェクトを受け取るので、操作を記述する。以下は、接続を受け付けるサンプル。

chrome.extension.onConnect.addListener(function(port) {
  console.assert(port.name == "knockknock");
  port.onMessage.addListener(function(msg) {
    if (msg.joke == "Knock knock")
      port.postMessage({question: "Who's there?"});
    else if (msg.answer == "Madame")
      port.postMessage({question: "Madame who?"});
    else if (msg.answer == "Madame... Bovary")
      port.postMessage({question: "I don't get it."});
  });
});

接続が切られたことを検地したい場合があるかもしれない。そのような場合は Port.onDisconnectイベントを用いる。このイベントは接続の一方が Port.disconnect メソッドを呼ぶか、接続しているページが閉じられたり他のページに移るなどの理由でUnloadされた際に呼び出される。onDisconnectは、渡されたポートが切断された時の1度だけ呼ばれる。

拡張機能間の通信

拡張機能内のコンポーネント間でメッセージを送るだけでなく、異なった拡張機能同士でメッセージAPIを介して通信することも出来る。これにより、他の拡張機能から利用できる開かれたAPIを公開することが可能になる。

受信の方法は、 extension.onRequestExternalextension.onConnectExternal を使うことを除けば拡張機能内での通信と同様である。以下に、単発通信の例を示す。

chrome.extension.onRequestExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id == blacklistedExtension)
      sendResponse({});  // 特定の拡張機能からのアクセスは拒絶する
    else if (request.getTargetData)
      sendResponse({targetData: targetData});
    else if (request.activateLasers) {
      var success = activateLasers();
      sendResponse({activateLasers: success});
    }
  });

次は、永続通信の例。

chrome.extension.onConnectExternal.addListener(function(port) {
  port.onMessage.addListener(function(msg) {
    // コールバック関数内の例は、他のサンプルを参照のこと
  });
});

同様に、送信も拡張機能内でのそれと変わることは無い。唯一の違いは、送信先の拡張機能のIDを指定する必要がある点だけである。以下に例を挙げる。

// 接続先の拡張機能ID
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// 単発通信
chrome.extension.sendRequest(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.extension.sendRequest(laserExtensionId, {activateLasers: true});
  });

// 永続通信用の接続を確立する
var port = chrome.extension.connect(laserExtensionId);
port.postMessage(...);

セキュリティへの配慮

コンテント・スクリプトや他の拡張機能からメッセージを受け取る場合、XSSの被害にあわないよう注意しなければならない。
コンテント・スクリプトで同様の例を挙げているので、参考にされたい。