訊息傳遞

訊息 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) {
  fetch(message.url)
    .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({
  url: 'https://example.com'
});

呼叫事件監聽器時,系統會將 sendResponse 函式做為第三個參數傳遞。這是可呼叫的函式,用於提供回覆。根據預設,sendResponse 回呼必須同步呼叫。如要執行非同步工作,取得傳遞至 sendResponse 的值,必須從事件監聽器傳回字面值 true (不只是真值)。這麼做會讓訊息管道對另一端保持開啟,直到呼叫 sendResponse 為止。

如果您呼叫 sendResponse 時未提供任何參數,系統會傳送 null 做為回應。

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

長期連線

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

您可以傳遞含有 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 序列化。也就是說,訊息 (和收件者提供的回覆) 可以包含任何有效的 JSON 值 (空值、布林值、數字、字串、陣列或物件)。其他值會強制轉換為可序列化的值。

值得注意的是,這與其他瀏覽器不同,後者會使用結構化複製演算法實作相同的 API。

通訊埠生命週期

連接埠的設計是為了在擴充功能的不同部分之間,提供雙向通訊機制。當擴充功能的一部分呼叫 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;
});