訊息傳遞

由於內容指令碼是在網頁的內容中執行,而不是在執行這些指令碼的擴充功能中執行,因此通常需要與擴充功能的其他部分進行通訊。舉例來說,RSS 閱讀器擴充功能可能會使用內容指令碼,偵測網頁上是否有 RSS 動態消息,然後通知服務工作者顯示該網頁的動作圖示。

這項通訊功能會使用訊息傳遞功能,讓擴充功能和內容指令碼都能收聽彼此的訊息,並在同一管道上回應。訊息可以包含任何有效的 JSON 物件 (空值、布林值、數字、字串、陣列或物件)。我們提供兩種訊息傳遞 API:一種用於一次性要求,另一種則用於長效連線,可傳送多封郵件。如要瞭解如何在擴充功能之間傳送訊息,請參閱「跨擴充功能訊息」一節。

一次性要求

如要將單一訊息傳送至擴充功能的其他部分,並視需要取得回應,請呼叫 runtime.sendMessage()tabs.sendMessage()。這些方法可讓您從內容指令碼傳送一次性的 JSON 序列化訊息,或是從擴充功能傳送至內容指令碼。如要處理回應,請使用傳回的承諾。為了與舊版擴充功能相容,您可以改為將回呼做為最後一個引數傳遞。您無法在同一個呼叫中使用承諾和回呼。

傳送訊息時,系統會將選用的第三個引數 sendResponse 傳遞給處理訊息的事件監聽器。這個函式會採用可序列化的 JSON 物件,做為傳送訊息函式的傳回值。根據預設,sendResponse 回呼必須以同步方式呼叫。如果您想執行非同步工作,以便取得傳遞至 sendResponse 的值,則必須從事件事件監聽器傳回文字常值 true (而非僅傳回真值)。這麼做會讓訊息管道保持開啟,直到呼叫 sendResponse 為止。

// Event listener
function handleMessages(message, sender, sendResponse) {

  fetch(message.url)
    .then((response) => sendResponse({statusCode: response.status}))

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

// Message sender
  const {statusCode} = await chrome.runtime.sendMessage({
    url: 'https://example.com'
  });

如要瞭解如何將回呼轉換為承諾,並在擴充功能中使用,請參閱 Manifest V3 遷移指南

透過內容指令碼傳送要求的做法如下:

content-script.js:

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

如果您想同步回覆訊息,只要在收到回應後呼叫 sendResponse,然後傳回 false 即可表示已完成。如要非同步回應,請傳回 true,讓 sendResponse 回呼保持啟用狀態,直到您準備使用為止。不支援非同步函式,因為這類函式會傳回系統不支援的 Promise。

如要向內容指令碼傳送要求,請指定要求適用於哪個分頁,如以下所示。這個範例適用於 Service Worker、彈出式視窗,以及以分頁開啟的 chrome-extension:// 頁面。

(async () => {
  const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
  const response = await chrome.tabs.sendMessage(tab.id, {greeting: "hello"});
  // do something with response here, not outside the function
  console.log(response);
})();

如要接收訊息,請設定 runtime.onMessage 事件監聽器。擴充功能和內容指令碼都會使用以下程式碼:

content-script.js 或 service-worker.js:

chrome.runtime.onMessage.addListener(
  function(request, sender, sendResponse) {
    console.log(sender.tab ?
                "from a content script:" + sender.tab.url :
                "from the extension");
    if (request.greeting === "hello")
      sendResponse({farewell: "goodbye"});
  }
);

在前一個範例中,系統同步呼叫 sendResponse()。如要非同步使用 sendResponse(),請將 return true; 新增至 onMessage 事件處理常式。

如果有多個網頁正在監聽 onMessage 事件,只有第一個針對特定事件呼叫 sendResponse() 的網頁才能成功傳送回應。系統會忽略對該事件的所有其他回應。

長期連線

如要建立可重複使用的長效訊息傳遞管道,請呼叫 runtime.connect(),將訊息從內容指令碼傳遞至擴充功能頁面,或是呼叫 tabs.connect(),將訊息從擴充功能頁面傳遞至內容指令碼。您可以為管道命名,以便區分不同類型的連線。

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

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

請使用以下程式碼,從內容指令碼開啟管道,並傳送及接收訊息:

content-script.js:

var port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
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"});
});

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

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

service-worker.js:

chrome.runtime.onConnect.addListener(function(port) {
  console.assert(port.name === "knockknock");
  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."});
  });
});

通訊埠生命週期

端口是擴充功能不同部分之間的雙向通訊方法。頂層框架是可使用端口的擴充功能中最小部分。當擴充功能的一部分呼叫 tabs.connect()runtime.connect()runtime.connectNative() 時,會建立一個連接埠,可立即使用 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 === blocklistedExtension)
      return;  // don't allow this extension access
    else if (request.getTargetData)
      sendResponse({targetData: targetData});
    else if (request.activateLasers) {
      var 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.
var 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:
var 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" 等主機名稱模式。自 Chrome 107 起,您可以使用 <all_urls> 存取所有網域。請注意,由於這會影響所有主機,因此使用這項功能的擴充功能可能需要較長時間才能通過 Chrome 線上應用程式商店審查。

使用 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.
  var 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!
  var 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;
});