【Chrome拡張機能】メッセージパッシングについて
-
JSONオブジェクトをやり取りできる
-
ワンタイムリクエスト用のシンプルなAPI(
runtime.sendMessage()
) -
service_workerで複数のメッセージを区別するときは case を使って区別する
-
長期接続用の複雑なAPI(
runtime.connect()
、) -
他の拡張機能のIDを知っていればその拡張機能にメッセージを送れる(
runtime.onMessageExternal()
、runtime.onConnectExternal
) -
例1のようにDOMの要素に対してイベントを追加する場合と、例2のようにChromeAPIのメッセージパッシングで使う
addEeventListener
はもちろん別の関数だ。前者は要素に対してイベントリスナーを定義している。後者はChromeAPIコンテキストでメッセージを受け取るためのイベントリスナーを設定している。どちらのaddEventListener()
も、使われるコンテキストや目的が異なるが、どちらも特定のイベントが発火したときに指定したリスナー(コールバック関数)を実行するという共通の動作を持つ。
イベントの扱い方は Webサイトと、Node.jsと、Chrome拡張機能では記法が異なる。(ドキュメントでも言及されている。)
その他の例として、WebExtensions と呼ばれる技術を使って、クロスブラウザーアドオン (ブラウザーの機能拡張) を JavaScript で作成できます。
イベントモデルは Web イベントモデルと似ていますが、ほんの少し違いがあります
(イベントリスナーのプロパティはキャメルケース (例 onmessage でなく onMessage) で命名されていて、addListener 関数で結び付ける必要があります。
例として runtime.onMessage page を確認してください。
https://developer.mozilla.org/ja/docs/Learn/JavaScript/Building_blocks/Events
- DOMの
addEventListener()
は引数の中にイベントとリスナー(コールバック関数)を定義する。一方、chrome.runtime.onMessage.addListener()の引数にはリスナー(コールバック関数)をいれ、リスナー(コールバック関数)の引数でイベントを取り出せる。DOMの場合のイベント名はDOMをクリックしたときに発生する"click"イベントや"blur"など、すでに定義されているイベントのみを取り扱う。一方、ChromeAPIではイベント名はこちらで指定することになっている。
サンプルコード
{
"manifest_version": 3,
"name": "Message Passing Sample",
"version": "1.0",
"description": "Sample extension for message passing between content script and service worker",
"permissions": ["storage", "activeTab"],
"background": {
"service_worker": "background.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
]
}
// コンテンツスクリプトでボタンを定義することで、コンテンツスクリプトとバックグラウンドだけでメッセージパッシングを実現してる。
const button = document.createElement("button");
button.textContent = "Click me!";
document.body.appendChild(button);
button.addEventListener("click", function () {
chrome.runtime.sendMessage(
{ action: "storeData", data: "Sample Data" },
function (response) {
alert(response.data);
}
);
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "storeDataa") {
chrome.storage.local.set({ storedData: message.data }, function () {
if (chrome.runtime.lastError) {
sendResponse({ status: false, data: chrome.runtime.lastError });
} else {
sendResponse({ status: true, data: "データを保存しました!" });
}
});
}
return true;
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {}
メッセージの受信方法の説明をしたいのに一行目からとても長い。しかもChromeAPIばかりなので初心者には理解する手掛かりすらない。これはどうしようもないので、メッセージを受信するときの構文として考えてもいい。
onMessage
はメッセージが送られてきたときに発生するイベントだ。下の図のようにコンテンツスクリプト、バックグラウンド、ポップアップのいずれかからメッセージが送られてきたときに発火する。
onMessage
イベントが持つメソッドは次の3つがある。
メソッド名 | 概要 |
---|---|
addListener() |
リスナーをこのイベントを定義する |
removeListener() |
このイベントの受け取りを中止する |
hasListener() |
リスナーがこのイベントに登録されているかどうかを確認する |
メッセージを受け取って決められた処理を実行したいときに使うのがaddEventListener()
だ。
addListener()
は引数にコールバック関数をとり、そのコールバック関数は以下の3つの引数をとる。
構文: addListener()
chrome.runtime.onMessage.addListener(message, sender, sendResponse) -> {
... statement ...
}
引数名 | 型 | 概要 |
---|---|---|
message |
object |
送信側から送られてくるメッセージ。 |
sender |
MessageSender |
runtime.MessageSender オブジェクト。メッセージの送信側を表す |
sendResponse |
fuction |
送信側に送るメッセージ。。1つのメッセージ内で一回しか実行できない。オブジェクトでも文字列でもいい。データクローンアルゴリズムというのを使っていて、柔軟に対処できるそうだ。MDNのドキュメント |
ここでは送られてきたメッセージをmessage.action
と、message.data
の2箇所で取り出している。送信側で{action: 'storeData', data: 'Sample Data'}
というオブジェクトを送ったからだ。メッセージはobject
型であればキーとバリューは自由に決めていいので、仮に{ syurui: "データ保存", okurudata: "保存するデータ" }
のようなデータを送ったら、受信側ではmessage.shurui
、message.okurudata
と書いて取り出す。
if (message.action === "storeDataa") {
ここで、message.action
で送られてきたメッセージの種類を取り出して、実行する処理を分けている。今回は1つしかメッセージを送っていないのでif文は必要ないが、説明のために書いている。
if文で分岐をするのは、複数メッセージの受信処理に対応するためだ。データを保存する以外でも、APIを使って外部サイトから情報を取ってきたり、訪れているサイトの認識など、メッセージパッシングを使うケースは出てくる。受け取るメッセージごとに chrome.runtime.onMessage.addListener()
を書くのは冗長になる。そこでif文やswitch文を使い、メッセージの種類ごとに実行するコードを分けることによってコードが簡略化される。
if (chrome.runtime.lastError)
この箇所ではエラーが起こった場合の処理を書いている。runtime.lastError
プロパティはコールバック関数のある非同期APIでエラーメッセージを伝えるときに使う。chrome.storage.local.set()
はコールバック関数を持つのでエラーの分岐にはruntime.lastError
を用いる。エラーがなければこの値はundefined
となっているのでif文では偽と評価され、else文のブロックが実行される。
ちなみにPromiseを使った非同期APIはruntime.lastError
プロパティを使わず、エラーハンドラーを使う。
runtime.lastError のドキュメント
sendResponse({ status: false, data: chrome.runtime.lastError });
return true
これはChromeAPI固有のバグだそう。
onmessage.addlistener return true;
で検索したら出てくる。
こんなエラーが出てくる
Unchecked runtime.lastError: The message port closed before a response was received.
MDNによると
非同期的に応答するには、次のどちらかを実行します。
sendResponse() に対する参照を保持したままリスナー関数から true を返す。そうすると、リスナー関数から復帰した後でも sendResponse() が実行できます。
リスナー関数から Promise を返して、応答の準備ができたときにその Promise を解決する。こちらがより好ましい方法です。
https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage#引数
と書いているのだが、addListener()内部でPromiseを使っている人でも、return true をしている。わからん。async, await は使えないのははっきりしている(ドキュメントに書いてるし、ブログでの失敗例はほとんどasync await を使おうとしているのが原因)
- 同期的に応答するか、非同期で応答するかによって書くことは変わる
- 返り値は
Promise
かboolean
同期か非同期により書き方が変わる
余談: イベント登録は「イベント名+イベントハンドラー」の2点セット!
そもそもJavaScriptでのon*
という命名規則はイベントハンドラープロパティ名で使われる。たとえば、要素をクリックしたときに実行されるのがonclick
プロパティで、画面をスクロールした際に実行されるのがonscroll
プロパティである。(イベント名はそれぞれclick
とscroll
)
なのでonMessage
はイベント名なのにon*
という名前がついていて命名規則に反する。
これをどう受け止めたらいいだろう。イベント登録は「イベント名+イベントハンドラー」の2点セットだと考えることにしている。
というのも、onMessage
の直後にイベントハンドラーを定義するaddListener()
が書かれるから
だ。
上で引用したようにイベント処理は、Web、Node.js、拡張機能で書き方がバラバラだ。
しかしいずれの分野であっても、イベント処理は「イベント名+イベントハンドラーの2点セット」で成り立っている。
拡張機能の場合は先ほど書いたようにonMessage.addListener()
と、「イベント名+イベントハンドラー」の2点セットそのものだ。
要素にイベントハンドラーを登録するときelement.addEventListener("click", () => {})
と書くが、これも「イベント名+イベントハンドラー」そのもの。
Node.js も eventEmitter.on("イベント名", イベントハンドラー)という記法で、やはり「イベント名+イベントハンドラー」という書き方をしている
よってイベント処理は「イベント名+イベントハンドラー」の2点セットと考えるのが辻褄に合う。
Discussion