Chrome extension context 下的 onMessage asynchronous sendResponse

當年(其實也就是幾個月以前)在製作 extension 某個功能的時候需要呼叫 chrome.tabs API 來拿一些資料再送回去給 content,於是就寫出了像下面這樣的 code:

/* file: content_script.js */

chrome.runtime.postMessage({action: 'tabs information'}, function(res) {
  /**
   * Do something with tabs information
   */
});
/* file: background.js */

chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
  if (request.action === 'tabs information') {
    // Query all tabs for information
    chrome.tabs.query({}, function (tabs) {
      var info = [];

      tabs.forEach(function (tab) {
        info.push({
          id: tab.id,
          title: tab.title,
          url: tab.url
        });
      });

      // Send it back with `sendResponse` function
      sendResponse(info);
    });
  }
});

看起來沒什麼大問題,但很遺憾的,這樣並不 work 。

我當時百思不得其解,用 Chrome DevTools 不停的 debug,花了一個下午的時間直到我在 chrome.runtime.onMessage 的文件裡面看到…

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).

看來是預設的情況之下, event listener 跑完就會把 sendResponse 用的 message channel 關起來,因此如果你需要 sendResponse 是在 asynchronous 的情況下被呼叫的話(基本上大部分的 chrome.* API 都是 asynchronous callback 的形式),在 event listener 的最後需要 return true

所以只要稍微修改一下 background.js 就行了!

/* file: background.js */

chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
  if (request.action === 'tabs information') {
    // Query all tabs for information
    chrome.tabs.query({}, function (tabs) {
      var info = [];

      tabs.forEach(function (tab) {
        info.push({
          id: tab.id,
          title: tab.title,
          url: tab.url
        });
      });

      // Send it back with `sendResponse` function
      sendResponse(info);
    });
    
    // Keep the message channel open until the `sendResponse` is called
    return true;
  }
});

一行,一下午。