訊息傳遞

訊息 API 可讓您在與擴充功能相關聯的環境中執行的不同指令碼之間進行通訊。包括服務工作站、chrome-extension://網頁和內容指令碼之間的通訊。舉例來說,RSS 閱讀器擴充功能可能會使用內容指令碼偵測網頁上是否有 RSS 動態消息,然後通知服務工作人員更新該網頁的動作圖示。

訊息傳遞 API 有兩種:一種用於一次性要求,另一種則較為複雜,用於長期連線,可傳送多則訊息。

如要瞭解如何在擴充功能之間傳送訊息,請參閱「跨擴充功能訊息」一節。

一次性要求

如要將單一訊息傳送至擴充功能的其他部分,並視需要取得回應,請呼叫 runtime.sendMessage()tabs.sendMessage()。這些方法可讓您從內容指令碼傳送一次性 JSON 可序列化訊息至擴充功能,或從擴充功能傳送至內容指令碼。這兩個 API 都會傳回 Promise,並解析為收件者提供的回應。

從內容指令碼傳送要求如下所示:

content-script.js:

(async () => {
  const response = await chrome.runtime.sendMessage({greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

回應

如要監聽訊息,請使用 chrome.runtime.onMessage 事件:

// Event listener
function handleMessages(message, sender, sendResponse) {
  if (message !== 'get-status') return;

  fetch('https://example.com')
    .then((response) => sendResponse({statusCode: response.status}))

  // Since `fetch` is asynchronous, must return an explicit `true`
  return true;
}

chrome.runtime.onMessage.addListener(handleMessages);

// From the sender's context...
const {statusCode} = await chrome.runtime.sendMessage('get-status');

呼叫事件監聽器時,系統會將 sendResponse 函式做為第三個參數傳遞。這項函式可呼叫來提供回覆。根據預設,sendResponse 回呼必須同步呼叫。

如果您在沒有任何參數的情況下呼叫 sendResponse,系統會傳送 null 做為回應。

如要非同步傳送回覆,可以選擇傳回 true 或傳回 Promise。

傳回 true

如要使用 sendResponse() 以非同步方式回應,請從事件監聽器傳回文字 true (不只是真值)。這樣做會讓訊息管道對另一端保持開啟,直到呼叫 sendResponse 為止,因此您稍後可以呼叫該管道。

退回承諾

從 Chrome 144 開始,您可以從訊息監聽器傳回 Promise,以非同步方式回應。如果 Promise 會解析,系統會將解析值做為回應傳送。

如果 Promise 遭到拒絕,系統會拒絕傳送者的 sendMessage() 呼叫,並顯示錯誤訊息。詳情和範例請參閱「錯誤處理」一節。

以下範例說明如何傳回可解析或拒絕的 Promise:

// Event listener
function handleMessages(message, sender, sendResponse) {
  // Return a promise that wraps fetch
  // If the response is OK, resolve with the status. If it's not OK then reject
  // with the network error that prevents the fetch from completing.
  return new Promise((resolve, reject) => {
    fetch('https://example.com')
      .then(response => {
        if (!response.ok) {
          reject(response);
        } else {
          resolve(response.status);
        }
      })
      .catch(error => {
        reject(error);
      });
  });
}
chrome.runtime.onMessage.addListener(handleMessages);

您也可以將事件監聽器宣告為 async,以傳回 Promise:

chrome.runtime.onMessage.addListener(async function(message, sender) {
  const response = await fetch('https://example.com');
  if (!response.ok) {
    // rejects the promise returned by `async function`.
    throw new Error(`Fetch failed: ${response.status}`);
  }
  // resolves the promise returned by `async function`.
  return {statusCode: response.status};
});

傳回 Promise:async 函式注意事項

請注意,即使沒有 return 陳述式,做為接聽程式的 async 函式一律會傳回 Promise。如果 async 監聽器未傳回值,其 Promise 會隱含解析為 undefined,並將 null 做為回應傳送給傳送者。如果有多個接聽程式,可能會導致非預期的行為:

// content_script.js
function handleResponse(message) {
    // The first listener promise resolves to `undefined` before the second
    // listener can respond. When a listener responds with `undefined`, Chrome
    // sends null as the response.
    console.assert(message === null);
}
function notifyBackgroundPage() {
    const sending = chrome.runtime.sendMessage('test');
    sending.then(handleResponse);
}
notifyBackgroundPage();

// background.js
chrome.runtime.onMessage.addListener(async (message) => {
    // This just causes the function to pause for a millisecond, but the promise
    // is *not* returned from the listener so it doesn't act as a response.
    await new Promise(resolve => {
        setTimeout(resolve, 1, 'OK');
    });
   // `async` functions always return promises. So once we
   // reach here there is an implicit `return undefined;`. Chrome translates
   // `undefined` responses to `null`.
});

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  return new Promise((resolve) => {
    setTimeout(resolve, 1000, 'response');
  });
});

處理錯誤

從 Chrome 144 開始,如果 onMessage 監聽器擲回錯誤 (同步或非同步,方法是傳回遭拒絕的 Promise),傳送端 sendMessage() 傳回的 Promise 會連同錯誤訊息一併遭拒絕。如果監聽器嘗試傳回無法JSON 序列化的回應,但未擷取產生的 TypeError,也可能發生這種情況。

如果監聽器傳回遭拒的 Promise,則必須使用 Error 例項拒絕,寄件者才能收到該錯誤訊息。如果 Promise 因任何其他值 (例如 nullundefined) 而遭到拒絕,sendMessage() 會改為拒絕並顯示一般錯誤訊息。

如果為 onMessage 註冊多個接聽程式,只有第一個回應、拒絕或擲回錯誤的接聽程式會影響傳送者;所有其他接聽程式都會執行,但結果會遭到忽略。

範例

如果監聽器傳回遭拒的 Promise,系統會拒絕 sendMessage()

// sender.js
try {
  await chrome.runtime.sendMessage('test');
} catch (e) {
  console.log(e.message); // "some error"
}

// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  return Promise.reject(new Error('some error'));
});

如果監聽器傳回無法序列化的值,sendMessage() 會遭到拒絕:

// sender.js
try {
  await chrome.runtime.sendMessage('test');
} catch (e) {
  console.log(e.message); // "Error: Could not serialize message."
}

// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  sendResponse(() => {}); // Functions are not serializable
  return true; // Keep channel open for async sendResponse
});

如果事件監聽器在任何其他事件監聽器回應之前,同步擲回錯誤,系統就會拒絕事件監聽器的 sendMessage() Promise:

// sender.js
try {
  await chrome.runtime.sendMessage('test');
} catch (e) {
  console.log(e.message); // "error!"
}

// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  throw new Error('error!');
});

不過,如果其中一個接聽程式在另一個接聽程式擲回錯誤之前回應,sendMessage() 就會成功:

// sender.js
const response = await chrome.runtime.sendMessage('test');
console.log(response); // "OK"

// listener.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  sendResponse('OK');
});
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  throw new Error('error!');
});

長期連線

如要建立可重複使用的長期訊息傳遞管道,請呼叫:

您可以傳遞含有 name 鍵的選項參數,為管道命名,藉此區分不同類型的連線:

const port = chrome.runtime.connect({name: "example"});

自動填寫表單的擴充功能就是長期連線的潛在用途之一。內容指令碼可能會為特定登入作業開啟擴充功能頁面的管道,並為網頁上的每個輸入元素傳送訊息給擴充功能,要求填入表單資料。擴充功能可透過共用連線,在擴充功能元件之間共用狀態。

建立連線時,每個端點都會獲派 runtime.Port 物件,透過該連線傳送及接收訊息。

使用下列程式碼從內容指令碼開啟管道,並傳送及監聽訊息:

content-script.js:

const port = chrome.runtime.connect({name: "knockknock"});
port.onMessage.addListener(function(msg) {
  if (msg.question === "Who's there?") {
    port.postMessage({answer: "Madame"});
  } else if (msg.question === "Madame who?") {
    port.postMessage({answer: "Madame... Bovary"});
  }
});
port.postMessage({joke: "Knock knock"});

如要從擴充功能傳送要求至內容指令碼,請將上一個範例中對 runtime.connect() 的呼叫替換為 tabs.connect()

如要處理內容指令碼或擴充功能網頁的連入連線,請設定 runtime.onConnect 事件監聽器。當擴充功能的其他部分呼叫 connect() 時,系統會啟動這個事件和 runtime.Port 物件。回應連入連線的程式碼如下所示:

service-worker.js:

chrome.runtime.onConnect.addListener(function(port) {
  if (port.name !== "knockknock") {
    return;
  }
  port.onMessage.addListener(function(msg) {
    if (msg.joke === "Knock knock") {
      port.postMessage({question: "Who's there?"});
    } else if (msg.answer === "Madame") {
      port.postMessage({question: "Madame who?"});
    } else if (msg.answer === "Madame... Bovary") {
      port.postMessage({question: "I don't get it."});
    }
  });
});

序列化

在 Chrome 中,訊息傳遞 API 會使用 JSON 序列化。值得注意的是,其他瀏覽器會使用結構化複製演算法實作相同的 API,這與 Chrome 的做法不同。

也就是說,訊息 (和收件者提供的回覆) 可以包含任何有效 JSON.stringify() 值。其他值會強制轉換為可序列化的值 (特別是 undefined 會序列化為 null);

郵件大小限制

訊息大小上限為 64 MiB。

通訊埠生命週期

連接埠的設計宗旨是做為擴充功能不同部分之間的雙向通訊機制。當擴充功能的一部分呼叫 tabs.connect()runtime.connect()runtime.connectNative() 時,會建立 Port,可立即使用 postMessage() 傳送訊息。

如果分頁中有多個框架,呼叫 tabs.connect() 會針對分頁中的每個框架,各觸發一次 runtime.onConnect 事件。同樣地,如果呼叫 runtime.connect(),則擴充功能程序中的每個影格都可以觸發一次 onConnect 事件。

您可能想瞭解連線何時關閉,例如為每個開啟的通訊埠維護個別狀態時。如要這麼做,請監聽 runtime.Port.onDisconnect 事件。當管道另一端沒有有效連接埠時,就會觸發這個事件,可能原因如下:

  • 另一端沒有 runtime.onConnect 的接聽者。
  • 含有通訊埠的分頁會卸載 (例如,如果分頁已導覽)。
  • 呼叫 connect() 的影格已卸載。
  • 已卸載所有收到連接埠的影格 (透過 runtime.onConnect)。
  • runtime.Port.disconnect() 是由另一端呼叫。如果 connect() 呼叫在接收端產生多個通訊埠,且在其中任一通訊埠上呼叫 disconnect(),則 onDisconnect 事件只會在傳送通訊埠觸發,不會在其他通訊埠觸發。

跨擴充功能傳訊

除了在擴充功能的不同元件之間傳送訊息,您也可以使用訊息 API 與其他擴充功能通訊。這樣一來,您就能公開 API,供其他擴充功能使用。

如要監聽來自其他擴充功能的連線和要求,請使用 runtime.onMessageExternalruntime.onConnectExternal 方法。以下是各項的範例:

service-worker.js

// For a single request:
chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.id !== allowlistedExtension) {
      return; // don't allow this extension access
    }
    if (request.getTargetData) {
      sendResponse({ targetData: targetData });
    } else if (request.activateLasers) {
      const success = activateLasers();
      sendResponse({ activateLasers: success });
    }
  }
);

// For long-lived connections:
chrome.runtime.onConnectExternal.addListener(function(port) {
  port.onMessage.addListener(function(msg) {
    // See other examples for sample onMessage handlers.
  });
});

如要傳送訊息給其他擴充功能,請傳遞要通訊的擴充功能 ID,如下所示:

service-worker.js

// The ID of the extension we want to talk to.
const laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// For a minimal request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
  function(response) {
    if (targetInRange(response.targetData))
      chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
  }
);

// For a long-lived connection:
const port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);

從網頁傳送訊息

擴充功能也可以接收及回覆網頁的訊息。如要從網頁傳送訊息至擴充功能,請在 manifest.json 中使用 "externally_connectable" 資訊清單鍵,指定要允許哪些網站傳送訊息。例如:

manifest.json

"externally_connectable": {
  "matches": ["https://*.example.com/*"]
}

這樣一來,凡是符合您指定網址模式的網頁,都能使用訊息 API。網址模式至少須包含二級網域,也就是說,系統不支援「*」、「*.com」、「*.co.uk」和「*.appspot.com」等主機名稱模式。您可以使用 <all_urls> 存取所有網域。

使用 runtime.sendMessage()runtime.connect() API 將訊息傳送至特定擴充功能。例如:

webpage.js

// The ID of the extension we want to talk to.
const editorExtensionId = 'abcdefghijklmnoabcdefhijklmnoabc';

// Check if extension is installed
if (chrome && chrome.runtime) {
  // Make a request:
  chrome.runtime.sendMessage(
    editorExtensionId,
    {
      openUrlInEditor: url
    },
    (response) => {
      if (!response.success) handleError(url);
    }
  );
}

在擴充功能中,使用 runtime.onMessageExternalruntime.onConnectExternal API 監聽網頁傳來的訊息,如跨擴充功能訊息傳遞一文所述。範例如下:

service-worker.js

chrome.runtime.onMessageExternal.addListener(
  function(request, sender, sendResponse) {
    if (sender.url === blocklistedWebsite)
      return;  // don't allow this web page access
    if (request.openUrlInEditor)
      openUrl(request.openUrlInEditor);
  });

擴充功能無法將訊息從 傳送至網頁。

原生訊息傳遞

擴充功能可以與註冊為原生訊息主機的原生應用程式交換訊息。如要進一步瞭解這項功能,請參閱「原生訊息」。

安全性考量

以下是與訊息相關的幾項安全性考量。

內容指令碼較不可信

內容指令碼的可信度低於擴充功能服務工作人員。舉例來說,惡意網頁可能會危害執行內容指令碼的算繪程序。假設內容指令碼傳送的訊息可能由攻擊者製作,並確保驗證及清除所有輸入內容。假設傳送至內容指令碼的任何資料都可能洩漏至網頁。限制可由從內容指令碼收到的訊息觸發的權限動作範圍。

跨網站指令碼攻擊

請務必保護指令碼,防範跨網站指令碼。從不可靠的來源 (例如使用者輸入內容、透過內容指令碼的其他網站或 API) 接收資料時,請務必避免將資料解譯為 HTML,或以可能允許執行非預期程式碼的方式使用資料。

更安全的方法

盡可能使用不會執行指令碼的 API:

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // JSON.parse doesn't evaluate the attacker's scripts.
  const resp = JSON.parse(response.farewell);
});

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // innerText does not let the attacker inject HTML elements.
  document.getElementById("resp").innerText = response.farewell;
});
不安全的方法

請避免使用下列方法,以免擴充功能遭到攻擊:

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be evaluating a malicious script!
  const resp = eval(`(${response.farewell})`);
});

service-worker.js

chrome.tabs.sendMessage(tab.id, {greeting: "hello"}, function(response) {
  // WARNING! Might be injecting a malicious script!
  document.getElementById("resp").innerHTML = response.farewell;
});