#状況
Chrome拡張で、content_scripts側からBackgroundPage側へsendMessageした時、コールバックの引数がundefinedになってしまう
#症状
上図のように、backgroundPageで非同期処理(今回はchrome.tabs.getSelected)を行い、その結果をコールバック関数の引数に入れたいのですが……
{
"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"]
}]
}
chrome.runtime.sendMessage({
message: "message"
}, function(response) {
console.log(response);
alert(response);
});
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
と書くだけです。
chrome.runtime.onMessage.addListener(
function(request, sender, callback) { // 1
chrome.tabs.getSelected(function(tab) { // 2
callback(tab.title);
});
// 3
return true;
}
);