跳至主要内容

[Chrome Extension] Message Passing API

keywords
  • runtime.sendMessage(<message>, callback<response>), tabs.sendMessage(<tabId>, <message>, callback<response>)
  • runtime.onMessage.addListener(callback<request, sender, sendResponse>), runtime.onMessageExternal.addListener(callback<request, sender, sendResponse>)
  • runtime.connect(), runtime.onConnect.addListener(callback<port>)

⚠️ 重要觀念:區分 Content Script 和 Chrome Extension

Content Script 一般指的就是注入別人網頁的程式碼,但不完全是,因為我們可能會用 ContentScript 在別人的頁面中嵌入 iframe,而這個 iframe 的內容實際上是去抓取 chrome extension 內的檔案。因此要區分是 content script 和 chrome extension 最準確的方式是看執行該程式碼的位置是在別人頁面內(Content Script)還是 Chrome Extension 的檔案內(檔案是以 chrome-extension:// 開頭的都算是包含在 Chrome Extension 內)。

之所以要區分是屬於 content script 或 chrome extension 的原因在於 Message API 傳送訊息的方式會有所不同。

特別留意:如果 ContentScript 是用來載入 iframe,且 iframe 的 src 是來自 chrome 套件內部(該檔案是以 "chrome-extension://" 開頭的都算),則這個 iframe 的內容一樣算是在 chrome extension 內,因此一樣可以接收到 chrome.runtime.sendMessage() 發送過來的訊息。

TL;TR

一次性傳送

一次性傳送 message

// runtime.sendMessage(<message>, callback<response>)

// 向 chrome extension(該檔案是以 "chrome-extension://" 開頭的都算)傳送訊息
chrome.runtime.sendMessage({ from: 'popup.js' }, (response) => {
console.log('get response', response);
});

// 若是要向 contentScript 傳送訊息,需使用 chrome.tabs.sendMessage
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
chrome.tabs.sendMessage(tabs[0].id, { greeting: 'hello' }, function (response) {
console.log(response.farewell);
});
});

一次性接收 message

// 接收 message,在 chrome extension 或 contentScript 都通用
// runtime.onMessage.addListener(callback<request, sender, sendResponse>)
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
console.log('sender', sender); // {id: "...", url: "..."}

if (request.from === 'popup.js') {
// 可以在 sendMessage 的 callback 中取得,此 sendResponse 的內容
// 需要注意若在多個地方呼叫同時呼叫 sendResponse,將只會收到一個
sendResponse({
from: 'background.js',
});
}
});

chrome.runtime.onMessage.addListener([callback]) 的 callback function 不能隨意把它改成 async callback function 並且在裡面用 await,否則 sendMessage 那個可能會拿不到 sendResponse 傳過去的東西。

這可能是因為 chrome 會預期這個 callback function 應該要回傳的是 boolean,如果把它變成 async callback function,並在內部使用 await 則這個 function 總是會回傳 Promise。

onMessage 的 callback 不要使用 async...await

chrome.runtime.onMessage.addListener([callback]) 的 callback function 不能隨意把它改成 async callback function 並且在裡面用 await,否則 sendMessage 那個可能會拿不到 sendResponse 傳過去的東西。

例如,照著下面的寫法會拿不到 sendResponse 傳的訊息

onMessage 的 callback 不要使用 async...await
// ❌ 會拿不到 sendResponse 傳的訊息
chrome.runtime.onMessage.addListener(
async (message, sender, sendResponse) => {
if (message.greeting === 'tip') {
const tip = await chrome.storage.local.get('tip');

sendResponse(tip);
return true;
}
},
);

改成這樣寫的話就可以:

不要用 async...await
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.greeting === 'tip') {
chrome.storage.local.get('tip').then(sendResponse);

return true;
}
});

長時間連接

建立 channel port,並發送與接收 message

// 建立 port 的那支檔案可以直接發送和接收 message,不要寫在 background.js
const port = chrome.runtime.connect({ name: 'private channel' });

// 透過 port 傳送 message
port.postMessage({
from: 'popup.js',
title: 'Knock knock',
});

// 透過 port 監聽其他地方傳來的 message
port.onMessage.addListener(function (msg) {
if (msg.from === 'background.js') {
console.log('receive msg from background.js', msg);
}
});

接收 port 傳來的 message

// 不是 port 的那支檔案需要在 runtime.onConnect 的時候監聽 port
chrome.runtime.onConnect.addListener(function (port) {
console.assert(port.name === 'private channel');

// 取得 port 後在該 port 內接收和發送 message
port.onMessage.addListener(function (msg) {
port.postMessage({
from: 'background.js',
title: 'receive Knock Knock',
});
});
});

一次性的發送(Simple one-time requests)

keywords: runtime.sendMessage(), runtime.onMessage.addListener(), tabs.sendMessage()

如果你需要將訊息傳送到擴充功能中的某一個部分(並可能需要取得回應),你可以使用較精簡的 runtime.sendMessagetabs.sendMessage,這可以讓你從 content script 傳送一次性 JSON 訊息到擴充套件中,反過來的通訊方式也可以。另外有一個提供的一個 callback 作為參數可以在取得回應的時候執行。

如果要從 content script 發送 message 像這樣:

chrome.runtime.sendMessage({ greeting: 'hello' }, function (response) {
console.log(response.farewell);
});

如果要從擴充套件傳送訊息到 content script 的方式很類似,除了需要額外指定特定的分頁(tab)來傳送,下面的例子示範如何傳送訊息到目前 active 頁籤的 content script

// 從擴充套件向 contentScript 發送訊息
// tabs.sendMessage(<tabId>, <message>, callback<response>)
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
chrome.tabs.sendMessage(tabs[0].id, { greeting: 'hello' }, function (response) {
console.log(response.farewell);
});
});

在接收訊息的那端,你需要設定 runtime.onMessage 的事件監聽器來處理此訊息,不論是從 content script 或 extension page 的做法都一樣

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() 是以同步的方式呼叫,如果你希望以非同步的方式處理,則在 onMessage 的事件處理器(event handler)中回傳 true

如果很多頁都在監聽 onMessage 的事件,只有第一個呼叫 sendResponse() 會成功送出回應,其他的發送的 response 則都會被忽略。

sendResponse 這個 callback 只有在以同步的方式有效,否則 event handler 需要回傳 true 來指出它是以非同步的方式來做出回應。sendMessage 內的 callback 會在沒有 handler 回傳 true 或 sendResponse callback 被 garbage-collected 的情況下自動執行。

長時間的連接(Long-lived connections)

keywords: runtime.connect(), tabs.connect(), port.postMessage(), port.onMessage.addListener()

有時你需要更長時間的連接(connection),而不是單一的 request 和 response,這種情況下,你可以使用 runtime.connecttabs.connect 從 content script 到擴充套件知間開啟 long-lived channel,或者反過來也可以。這個 channel 可以選擇性地有自己的名字,讓你可以區分不同類型的連結。

一種常見的使用情況是「自動填寫表單」的擴充套件,從 content script 可以開啟一個 channel 到 extension page,並針對每一個 input element 傳送 message 到 extension,透過共享連結,可以讓擴充套件和 content script 間保持共享的資料狀態。

當建立一個連結時會得到一個 runtime.Port 物件,透過這個物件可以從這個連接中發送和接受訊息。

下面是從 content script 建立一個頻道(channel),並發送與接送 message 的例子

// contentScript.js
// 從 content script 建立 channel,並且監聽與發送 message
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' });
}
});

從擴充套件發送請求到 content script 的做法類似,除了你需要指定要連接到的頁籤,只需要將它改成 tabs.connect

為了要處理持續的連結,你需要設置 runtime.onConnect 的事件監聽器,這和在 content script 或其他 extension page 的方式相同,當 extension 中的其他部分呼叫 connect() 時,事件就會被觸發,搭配 runtime.Port 物件,你將可以透過該連結傳送和接收訊息,範例如下:

// 使用 onConnect 來監聽各個 port
chrome.runtime.onConnect.addListener(function (port) {
// 查看是哪個 port
console.assert(port.name == 'knockKnock');

// 根據 port 傳進來的 message 發送回應
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." });
}
});
});

Port 生命期(lifetime)

Ports 被設計用來讓 extension 內的部分進行雙向溝通(two-way communication),在擴充套件中最高層的(top-level)frame 被視為最小單位。透過呼叫 tabs.connect, runtime.connectruntime.connectNative, 可以建立一個 Port。這個 Port 可以馬上透過 postMessage 來發送 message。

如果在一個頁籤(tab)中有許多 frames 時,呼叫 tabs.connect 會同時觸發多個 runtime.onConnect 事件(在頁籤中每個 frame 一次),相似地,如果使用 runtime.connect ,那個 onConnect 事件可能會多次促發(在 extension process 中的每個 frame 一次)。

如果你想要在 connection 被關閉時被通知到,如此可以使用 runtime.Port.onDisconnect 事件,它會在 channel 另一端沒有有效的 ports 時被促發,這有可能是因為這裡所列的情況。

從網頁傳送 message 到 Extension(Sending messages from web pages to extension)

keywords: externally_connectable, chrome.runtime.onMessageExternal.addListener()
// manifest.json
"externally_connectable": {
"matches": ["*://*.example.com/*"]
}

從 WebPage 向 Extension 發送 message:

/* webpage.html */
// chrome extension id
const editorExtensionId = 'here_is_extension_id';

// 透過 chrome.runtime.sendMessage(extensionId, payload, callback) 傳送訊息
const url = window.location.href;
chrome.runtime.sendMessage(editorExtensionId, { from: url }, (response) => {
console.log('response', response);
});

在 Extension 接收 message:

// background.js
// 透過 chrome.runtime.onMessageExternal.addListener(callback<request, sender, sendResponse>)
// 取得 webpack 傳過來的訊息
chrome.runtime.onMessageExternal.addListener((request, sender, sendResponse) => {
// 只讓合法網站可以存取
if (sender.url == blocklistedWebsite) {
return;
}
if (request.openUrlInEditor) {
sendResponse({
greet: 'hello',
});
}
});

Example

Messaging Examples