由於內容指令碼是在網頁的內容中執行,而不是在執行這些指令碼的擴充功能中執行,因此通常需要與擴充功能的其他部分進行通訊。舉例來說,RSS 閱讀器擴充功能可能會使用內容指令碼,偵測網頁上是否有 RSS 動態消息,然後通知服務工作者顯示該網頁的動作圖示。
這種通訊會使用訊息傳遞功能,讓擴充功能和內容指令碼監聽彼此的訊息,並在同一個管道中回應。訊息可以包含任何有效的 JSON 物件 (空值、布林值、數字、字串、陣列或物件)。我們提供兩種訊息傳遞 API:一種用於一次性要求,另一種則用於長效連線,可傳送多封郵件。如要瞭解如何在擴充功能之間傳送訊息,請參閱「跨擴充功能訊息」一節。
一次性要求
如要將單一訊息傳送至擴充功能的其他部分,並視需要取得回應,請呼叫 runtime.sendMessage()
或 tabs.sendMessage()
。這些方法可讓您從內容指令碼傳送一次性的 JSON 序列化訊息,或是從擴充功能傳送至內容指令碼。如要處理回應,請使用傳回的承諾。為確保與舊版擴充功能的回溯相容性,您可以改為將回呼做為最後一個引數傳遞。您無法在同一個呼叫中使用承諾和回呼。
如要瞭解如何將回呼轉換為承諾值,以及如何在擴充功能中使用回呼,請參閱 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.onMessageExternal
或 runtime.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.onMessageExternal
或 runtime.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; });