Edited at

Chrome拡張でruntime.sendMessageのコールバックがうまく動作しない理由

More than 1 year has passed since last update.


状況

Chrome拡張で、content_scripts側からBackgroundPage側へsendMessageした時、コールバックの引数がundefinedになってしまう


症状

sendMessage.jpg

上図のように、backgroundPageで非同期処理(今回はchrome.tabs.getSelected)を行い、その結果をコールバック関数の引数に入れたいのですが……


manifest.json

{

"manifest_version": 2,
"name": "sendMessage Test",
"version": "0.1",
"author": "Tachibana446",
"description": "hoge",
"browser_action": {
"default_icon": {
"38": "icon_38.png"
},
"default_title": "Do it!"
},
"permissions": ["tabs", "http://*/*", "https://*/*"],
"background": {
"scripts": ["background.js"],
"presistent": false
},
"content_scripts": [{
"matches": ["https://www.google.co.jp/*"],
"js": ["content.js"]
}]
}


content.js

chrome.runtime.sendMessage({

message: "message"
}, function(response) {
console.log(response);
alert(response);
});



background.js

chrome.runtime.onMessage.addListener(

function(request, sender, callback) { // 1
chrome.tabs.getSelected(function(tab) { // 2
callback(tab.title);
});
// 3
}
);


(chrome.tabs.getSelectedは現在開いているタブを取得する関数です)

上のサンプルは現在のタブのタイトルをアラートダイアログで表示するだけのプログラムですが、実際に実行するとアラートにはundefinedと表示されてしまいます。


解説

まず、大前提としてjavascriptのなんか引数にfunction渡したりする系のものは非同期なんですね。

その辺を理解していなかったのでなかなかわかりませんでした。

https://developer.chrome.com/extensions/runtime#event-onMessage

そして上のこのページに解説がちゃんとあるんですよね。


Function to call (at most once) when you have a response. The argument should be any JSON-ifiable object. If you have more than one onMessage listener in the same document, then only one may send a response. This function becomes invalid when the event listener returns, unless you return true from the event listener to indicate you wish to send a response asynchronously (this will keep the message channel open to the other end until sendResponse is called).

The sendResponse parameter should be a function that looks like this:

function() {...};


unless you return trueのところを見てください。とは言え筆者は英語ができないのでざっくりですが、

「この(sendMessageの引数として与えられるコールバック)関数は、値を返した時点で無効になります。非同期の処理を同期的に待つときはtrueを返してください。」

というようなことが書いてあります。

つまり、非同期的な処理を行う前に関数の最後に到達してしまい、値(この場合明示的にreturnされていないのでundef)が返されるのでそこで処理が無効になってしまっていたのです。

具体的に説明しましょう。

先ほどのbackground.jsをご覧ください。

// 1のところで、リスナーの匿名関数がスタートします。

// 2のところで、getSelected関数(の引数に与えられた匿名関数)が非同期で行われます。

しかし非同期なので、その処理は一旦おいておいて、そのまま// 3のところへ行きます。

// 3以降、リスナーの関数は続きがないので、ここでリスナーの関数が終了し、既定値(たぶんundefined)が返戻されます。

値が返戻されたので、このリスナー関数は無効になり、待機していた非同期のgetSelectedの引数の匿名関数も処理されません。

そのままコールバックが呼び出されるので、期待していたタブのタイトルを取得できませんでした。


解決法

公式ドキュメントの通り、明示的にtrueを返してあげましょう。

先ほどの// 3のところにreturn trueと書くだけです。


background.js

chrome.runtime.onMessage.addListener(

function(request, sender, callback) { // 1
chrome.tabs.getSelected(function(tab) { // 2
callback(tab.title);
});
// 3
return true;
}
);