manifest.json 出發,為 Chrome、Edge、Firefox 與 Safari 建置並執行瀏覽器擴充功能,而下面的訊息傳遞 API 都是瀏覽器自身的 API。要試驗這些範例,請用 npx extension@latest create my-extension 建立一個專案。
傳遞訊息的三種方式
chrome.runtime.sendMessage。 一次性。它會送達每一個帶有 runtime.onMessage 監聽器的擴充功能情境,也就是 service worker、popup、options 頁面,以及任何其他已開啟的擴充功能頁面。它不會送達 content script。
chrome.tabs.sendMessage。 一次性,瞄準一個分頁。從 service worker 或另一個擴充功能頁面呼叫它,去送達那個分頁裡的 content script,還可以透過 frameId 只送達一個 frame。content script 必須已經在那裡執行。
chrome.runtime.connect 與 chrome.tabs.connect。 一個保持開啟的具名 port。兩端可以隨時投遞訊息,兩端也都透過 port.onDisconnect 得知拆除。
在你自己的情境之間傳訊息不需要額外權限。它需要的是對面有一個活著的監聽器。
非同步回應規則
runtime.onMessage 監聽器預設是同步回覆的。它一回傳,通道就關閉,之後再呼叫 sendResponse 會被丟棄。回傳 true 才是讓通道保持開啟的做法:
async 監聽器函式回傳的是 Promise,而 Promise 不是 true,因此在 Chromium 上,一個稍後才呼叫 sendResponse 的 async 監聽器仍然會關閉通道。請像上面那樣在監聽器內部做非同步工作並回傳 true。在呼叫端這一側,省略回呼時 chrome.runtime.sendMessage 會回傳 Promise,所以那裡直接 await 即可,與以上這些無關。
給每則訊息一個明確的 type,在處理之前驗證酬載,並在做特權工作之前檢查 sender。content script 是你的程式碼,但它轉發的資料來自你無法控制的頁面。
驗證 content script 與背景之間的訊息
content script 執行在一個你無法控制的頁面裡。頁面可以透過 DOM 或window.postMessage 把任何東西交給它,而 content script 再把這些資料轉發給背景。正因為如此,Chrome 的文件把 content script 列為比 service worker 更不可信的一方。請把到達背景的每一則訊息都當作不可信的輸入來對待,就像伺服器對待請求本文那樣。
檢查是誰在呼叫。 每個監聽器都會收到一個 sender 物件,也就是 runtime.MessageSender 型別。以下是真正重要的欄位:
runtime.onMessage 只會因你自己擴充功能的情境而觸發,所以在那裡 sender.id 永遠是你自己的 id。來自另一個擴充功能、或來自 externally_connectable 放行的網頁的訊息,會改為到達 runtime.onMessageExternal,而那裡才是由 sender.id 和 sender.url 決定一切的地方。
先驗證形狀,再採取行動。 維護一份訊息類型允許清單,檢查每個欄位的型別和大小,其餘一律拒絕。絕不要把訊息裡的字串變成程式碼:不要 eval,不要 new Function,不要 innerHTML,也不要讓 chrome.scripting.executeScript 執行來自訊息的程式碼。寫入文字請用 textContent。
externally_connectable 鍵時,其他每個擴充功能都可以向你的擴充功能傳訊息,而任何網頁都不行。宣告這個鍵來收窄或放寬這一點。ids 列出允許連線的擴充功能,"*" 放行全部擴充功能。matches 列出可以帶著你的擴充功能 id 呼叫 runtime.sendMessage 的網頁。這些訊息會落到 runtime.onMessageExternal 和 runtime.onConnectExternal 上,絕不會落到 onMessage 上,因此請給它們獨立的監聽器,並把 sender.url 與你宣告的模式做比較。
runtime.onConnect 和 runtime.onConnectExternal 交付的 port 上會設定 port.sender,因此在 port 開啟時檢查一次 port.name 和送出方即可。此後 port 的身分是固定的,但到達 port.onMessage 的每個酬載仍然是頁面資料,所以要讓它們經過同一個型別守衛。收到第一則壞訊息時就呼叫 port.disconnect()。
Firefox 的不同之處:
sender.origin從 Firefox 126 起才存在。請從sender.url推導 origin,一行程式碼同時涵蓋舊版本和 Chrome。- Firefox 不支援
externally_connectable,因此網頁永遠無法向 Firefox 擴充功能傳訊息,runtime.onMessageExternal在那裡只會因另一個擴充功能而觸發。Safari 15.4 支援這個鍵,但只支援matches。 browser.runtime.onMessage會採納回傳的 Promise,下面的表格有說明。
extension dev 之下,重新載入橋接器會透過一個名為 __extjs-bridge-log__ 的 port 把主控台輸出轉發到 service worker。devtools 頁面則改用 runtime.sendMessage 轉發,訊息是一個帶 __extjsBridgeLog 鍵的物件。一個會檢查 port.name 的 runtime.onConnect 監聽器,以及一個會拒絕沒有已知 type 的訊息的 onMessage 監聽器,會把兩者都忽略掉。extension build 的輸出不包含這些內容。
各瀏覽器差異
在 Safari 上,在你授予擴充功能網站存取權之前 content script 不會執行,因此在那之前
tabs.sendMessage 沒有接收方。請見 Safari。
如果你的原始碼是以 browser.* 撰寫,同時也要建置 Chromium 目標,請傳入 --polyfill。請見 跨瀏覽器相容。
你會看到的主控台錯誤
把你看到的那一行複製去搜尋。每一行對應一個原因。Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.
沒有任何一方在監聽。要嘛目標情境沒有 runtime.onMessage 監聽器,要嘛對 tabs.sendMessage 而言,還沒有 content script 被注入那個分頁。在擴充功能載入之前就已開啟的分頁在重新載入前沒有 content script,而像 chrome:// 或擴充功能商店這樣的受限頁面永遠不會有。請先用 chrome.scripting.executeScript 注入,或者處理這次失敗,做法請見 在執行階段注入腳本。
Unchecked runtime.lastError: The message port closed before a response was received.
監聽器收到了訊息,卻在沒有讓通道保持開啟的情況下回傳了。請從監聽器回傳 true,或者同步作答。
Uncaught Error: Extension context invalidated.
擴充功能重新載入時,舊的 content script 還在頁面裡執行。那段程式碼現在指向一個不再存在的執行階段,它發出的每個 chrome.* 呼叫都會擲出錯誤。請重新載入分頁。在 extension dev 期間,這發生在一次被歸類為完整重新載入的變更之後,重新載入與 HMR 對此有說明。
Attempting to use a disconnected port object
在 onDisconnect 觸發之後,仍有人向這個 port 投遞訊息。請在 onDisconnect 處理常式裡清掉你的參照,需要時再開啟一個新的 port。
Extension.js 的做法
Extension.js 編譯呼叫這些 API 的程式碼,並不包裝它們。協定是你自己的。工具鏈改變的是開發循環:- 一次被歸類為
content-scripts的變更會重新注入受影響的項目並拆除上一次的掛載,因此監聽器是被取代而不是被疊加。一次被歸類為full的變更會重新載入擴充功能,而重新載入前就留在頁面上的 content script 會變成上面那種Extension context invalidated情況。請重新載入分頁。 - 在
extension dev之下,從scripts/資料夾注入的腳本會在編輯後被重播,因此你在被注入腳本裡開啟的 port 會由新的副本重新開啟。請見 特殊資料夾。 - service worker 與 content script 的主控台輸出會被轉發到同一個通道,因此跨越邊界的訊息更容易在單一串流裡追蹤。

