訊息傳遞

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

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

一次性要求

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

想瞭解如何將回呼轉換為承諾,以及如何在擴充功能中使用,請參閱 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);
})();

如要傳送要求至內容指令碼,請指定要求要套用的分頁,如下所示。這個範例適用於以分頁開啟的服務工作站、彈出式視窗和 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() 時,系統會建立 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 === 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 simple 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.
var editorExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";

// Make a simple request:
chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
  function(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);
  });

內建訊息傳遞

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

安全性考量

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

內容指令碼較不可靠

與擴充功能 Service Worker 相比,內容指令碼的可信度較低。 舉例來說,惡意網頁可能破壞執行內容指令碼的轉譯程序。假設來自內容指令碼的訊息可能是由攻擊者精心打造,因此請務必驗證並清理所有輸入內容。假設傳送至內容指令碼的任何資料可能會導致網頁外洩。限制可由內容指令碼接收的訊息觸發特殊權限動作的範圍。

跨網站指令碼攻擊

請務必避免指令碼遭到跨網站指令碼攻擊。接收不受信任的來源 (例如使用者輸入內容、透過內容指令碼或 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;
});