在 Manifest V3 中,chrome.runtime.onMessage 的監聽器

您遇到的問題——popup.jschrome.runtime.sendMessage 的回呼函數收到的 responseundefined——是一個在使用 Manifest V3 的 Service Worker (MV3) 時非常常見的非同步處理問題。

問題根源:

在 Manifest V3 中,chrome.runtime.onMessage 的監聽器(在您的 background.js 中)如果需要執行非同步操作(例如您的 await refreshAlarmList(true);),那麼必須從監聽器函數中 return true; 來告知 Chrome 擴充功能系統:「嘿,我這個訊息處理是異步的,請保持 sendResponse 管道開啟,直到我明確調用 sendResponse 為止。」

如果您沒有從異步的 onMessage 監聽器中 return true;,那麼在 await refreshAlarmList(true); 執行完成並調用 sendResponse({message:"ok"}); 之前onMessage 監聽器函數可能就已經執行完畢並返回了(預設返回 undefined)。Chrome 系統會認為訊息處理已經結束,於是關閉了與 popup.js 的通訊管道。這時,即使 background.js 後來調用了 sendResponsepopup.js 也收不到了,或者收到的就是 undefined

解決方案:

修改您的 background.js 中的 chrome.runtime.onMessage.addListener,讓它在處理異步操作時 return true;

修改後的 background.js

      chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { // 注意:這裡移除了 async 關鍵字,因為 return true 必須是同步返回的
  try {
    if (request.from == "popup") {
      // 異步操作開始
      refreshAlarmList(true)
        .then(() => {
          console.log("refresh alarm list ok");
          sendResponse({ message: "ok" });
        })
        .catch(error => {
          console.error("refreshAlarmList: 出錯: ", error);
          // 即使出錯,也應該調用 sendResponse,或者讓 popup 端處理超時
          sendResponse({ message: "error", error: error.message });
        });

      // *** 關鍵:告訴 Chrome 這是一個異步響應 ***
      return true;
    } else {
      // 對於同步響應,不需要 return true
      sendResponse({ message: "no" });
      return false; // 或者不 return 任何東西,但 return true 只在異步時需要
    }
  } catch (error) { // 這個 catch 捕獲的是 addListener 內部同步部分的錯誤,異步錯誤在 .catch 中處理
    console.error("onMessage listener synchronous error: ", error);
    // 也可以考慮在這裡 sendResponse 一個錯誤訊息
    return false; // 如果在這裡捕獲了同步錯誤,也要表明不是異步的
  }
});

// 假設 refreshAlarmList 是一個 async 函數
// async function refreshAlarmList(forceRefresh) { /* ... 您的邏輯 ... */ }
    

解釋修改:

  1. addListener 的回呼函數不再是 async 為了能夠同步地 return true;,我們將 async 關鍵字從 addListener 的回呼函數上移除。

  2. 使用 .then().catch() 處理 Promise: 由於 refreshAlarmList 是一個 async 函數(返回 Promise),我們使用 .then() 來處理成功的情況(並在其中調用 sendResponse),使用 .catch() 來處理失敗的情況(同樣在其中調用 sendResponse)。

  3. return true; 這是最重要的一步。在 if(request.from == "popup") 條件塊的末尾(在異步操作啟動之後),我們 return true;。這告訴 Chrome 保持通訊管道開啟,等待 sendResponse 被異步調用。

  4. 同步路徑的處理: 對於不需要異步操作的 else 分支,我們可以直接調用 sendResponse,並且可以 return false; 或不返回任何東西(因為預設返回 undefined,不是 true,所以管道會關閉)。

popup.js 中的程式碼可以保持不變。

關於「最近的更新造成的XD」:

雖然 Manifest V3 對 Service Worker 和訊息傳遞的要求已經存在一段時間了,但 Chrome 的更新確實可能讓一些之前「碰巧能工作」的邊緣情況失效,或者讓問題更明顯地暴露出來。return true; 的要求在 Manifest V3 的 Service Worker 環境下對於異步 onMessage 處理是一直存在的。

請嘗試修改您的 background.js,這應該能解決 responseundefined 的問題。



// background.js

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {

  if (request.from == "popup") {

    // 假設 refreshAlarmList 是一個 async 函數

    refreshAlarmList(true) // refreshAlarmList 內部可以使用 await getAlarmList() 等

      .then(() => {

        console.log("refresh alarm list ok");

        sendResponse({ message: "ok" });

      })

      .catch(error => {

        console.error("refreshAlarmList: 出錯: ", error);

        sendResponse({ message: "error", error: error.message });

      });

    return true; // 關鍵!

  } else {

    sendResponse({ message: "no" });

    // return false; // 或者不返回

  }

  // 注意:try...catch 最好放在 .then/.catch 內部或 refreshAlarmList 內部去處理異步錯誤

});


async function refreshAlarmList(forceRefresh) {

  try {

    console.log("Refreshing alarm list...");

    // 您可以在這裡面安全地使用 await getAlarmList()

    const list = await getAlarmList(); // 假設 getAlarmList 也是 async

    // ... 執行您的刷新邏輯 ...

    console.log("Alarm list refreshed successfully.");

  } catch (error) {

    console.error("Error in refreshAlarmList:", error);

    throw error; // 可以向上拋出,讓 .catch 捕獲

  }

}


async function getAlarmList(){ // 您的 async getAlarmList 函數

    try {

        const obj = await chrome.storage.local.get(['alarmlist']);

        return obj.alarmlist || {};

    } catch (error) {

        console.error("讀取 alarmlist 時出錯:", error);

        return {};

    }

}

留言

這個網誌中的熱門文章

Offscreen Canvas

外部網頁新增 Google 日曆行程(URL模板)

計算 Canvas 文字最大字體