Chrome Extension 的 background、content script、popup script 各自跑在獨立的 js 空間中,若要傳送資料只能透過 chrome 提供的 message api。可是在不同的情境中有時可以雙向發起訊息交換,有時只能單向,對於初學者而言有點複雜。於是決定統整成這一篇,希望能夠整理各種情境,提供後續開發參考使用。我們就一起來看看吧!
Chrome Message Api
簡單來說,Chrome 提供的 Message Api 分兩種類型:
- 傳送訊息至 background 的 api
- 傳送訊息至 content script 的 api
用法整理如下
傳送訊息至 Background
// send from extension inside
chrome.runtime.sendMessage(<data>);
// send from extension outside
chrome.runtime.sendMessage(<extension id>, <data>);
看到 runtime.sendMessage()
一定是 傳送訊息至 background
。
但依「傳送的來源」分成兩種:
- Extension 內部(如從 content script 或 popup script 發出)
- Extension 外部(如從 inject script 、 web app 或其他 extension)
兩者用法差異在於,如果從 extension 外部送訊息(如 2.),必須指定目標 background 所屬的 extension id
,若是從內部(如 1.)則不需指定。
傳送訊息至 Content Script
chrome.tabs.sendMessage(<tab id>, <data>);
看到 tabs.sendMessage()
一定是 傳送訊息至 content script
,因為每個 tab 都有額外一個 js 獨立空間跑 content script。若要傳送訊息至 content script,勢必指定接收端所屬的 tab id
。
宣告 Message Listener Handler
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
// do something at here...
sendResponse(<data>)
})
無論是在 background 或 content script 宣告 message listener handler,語法都是一樣的,同時也會有 sendResponse()
方法於 handler 回傳的參數中,提供回覆訊息至發送方使用。
Web Api – Window.postMessage()
web api 中的 postMessage()
通常用於 跨頁面溝通
,但我們可以用此方法實現 inject script 與 content script 互相溝通,補足 chrome message api 未提供的部分。
為何 inject script 可以透過 postMessage()
與 content script 溝通呢?雖然他們兩者是分別跑在不同的 js 空間中,但共用同一份 DOM
,因此兩者的 window
物件是相同的,可以透過同一個 window
物件發送與接收訊息!
其中需注意,在 handler 裡面要作訊息來源判斷,避免自己發送訊息後,自己的 handler 立刻收到訊息發生異常。
傳送訊息
window.postMessage(<data>);
調用 window.postMessage()
並傳入資料即可。
監聽訊息
window.addEventListener('message', (event) => {
// do something at here...
});
在接收方用 window.addEventListener()
方法宣告 message handler,並在裡面作來源判斷。
統整一下吧!
了解 Chrome 和 Web 提供的 Message Api,我們將 發送方
和 接收方
與使用的 api 統整成一個表格來觀察看看吧!
發送方 \ 接收方 | background | popup script | content script | inject script |
background | – | tabs.sendMessage | ||
popup script | runtime.sendMessage | – | tabs.sendMessage | |
content script | runtime.sendMessage | – | window.postMessage | |
inject script | runtime.sendMessage | window.postMessage | – |
從這個表格和先前討論可以確認,發送訊息所需用的 Message Api,與 接收方
是誰有關。
同時也能發現,有些 發送方
是不能發訊息給某些 接收方
的。將其真值表整理如下:
發送方 \ 接收方 | background | popup script | content script | inject script |
background | – | x | v | x |
popup script | v | – | v | x |
content script | v | x | – | v |
inject script | v | x | v | – |
從上表格可以知道
- popup script 不能透過 message handler
被動
接收別人發出訊息 - inject script 不能接收來自 background 和 popup script 發出的訊息
思考起來也合理,因為
- popup script 只有在使用者點 extension icon 跳出 popup 時才會啟動,因此無法在任意時刻監聽別人傳來的訊息。
- chrome message api 沒有提供傳送訊息至 inject script 的功能。web api 也僅能於共享 DOM 的 inject script 和 content script 之間互相溝通,因此這部份無法實現。
以下的官方示意圖也能輔佐此觀察:
程式碼範例
整理完各種情境後,我們一起來看看實際的寫法吧!
範例原始碼在此下載:github
從 background 發起訊息
傳送至 content script
在 background
async function sendMsgToContentScript() {
const tab = await getActiveTab();
const ret = await chrome.tabs.sendMessage(tab.id, {from: 'ping-ping from background'});
console.log('receive', ret);
}
在 content script
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
sendResponse({response: 'pone-pone from content script'})
})
實測結果如下:
background log
content script log
從 popup script 發起訊息
傳送至 background
在 popup script
async function sendMsgToBackground() {
const ret = await chrome.runtime.sendMessage({from: '[Demo] ping from popup script'});
console.log('[Demo] receive', ret);
}
在 background
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
console.log('receive from popup script', request);
sendResponse({response: "pong from background"});
});
實測結果如下:
popup 和 background log
傳送至 content script
在 popup script
async function sendMsgToContentScript() {
const tab = await getActiveTab();
const ret = await chrome.tabs.sendMessage(tab.id, {from: 'ping from popup script'});
console.log('[Demo] receive', ret);
}
在 content script
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
sendResponse({response: 'pone-pone from content script'})
})
實測結果如下:
popup log
content script log
從 content script 發起訊息
傳送至 background
在 content script
async function sendMsgToBackground() {
const ret = await chrome.runtime.sendMessage({from: '[Demo] ping from content script'});
console.log('[Demo] receive', ret);
}
在 background
chrome.runtime.onMessage.addListener((request, sender, sendResponse) =>
console.log(`receive from content script ${sender.tab.url}`, request);
sendResponse({response: "pong from background"});
sendMsgToContentScript();
});
實測結果如下:
content script log
background log
傳送至 inject script
在 content script
async function sendMsgToInjectScript() {
window.postMessage({msg: 'ping from inject script', from: 'content-script'});
}
在 inject script
window.addEventListener('message', (event) => {
if (event.source === window && event.data.from === 'content-script') {
console.log('[Demo] receive', event.data);
}
});
實測結果如下:
content script 和 inject script log
inject script 發起訊息
傳送至 background
因 chrome extension 權限規定,需要在 manifest.json 增加以下宣告,讓特定網域可以傳送訊息至 background:
"externally_connectable": {
"matches": ["<all_urls>"] // 使用特殊符號 <all_urls> 即可讓所有網域傳送訊息至 background
}
在 inject script
async function sendMsgToBackgroundScript() {p
const extensionId = getExtensionId();
// 於 inject script 使用 chrome message api 時
// 需注意這邊還沒有 promise 的寫法
chrome.runtime.sendMessage(extensionId, {from: 'ping from inject script'}, (ret) => {
console.log('[Demo] receive', ret);
});
}
在 background
chrome.runtime.onMessageExternal.addListener((request, sender, sendResponse) => {
console.log('[Demo] receive from other extension or web app', request);
sendResponse({response: "pong from background"});
});
實測結果如下:
inject script log
background log
傳送至 content script
在 inject script
async function sendMsgToContentScript() {
window.postMessage({msg: 'ping from inject script', from: 'inject-script'});
}
在 content script
window.addEventListener('message', (event) => {
if (event.source === window && event.data.from === 'inject-script') {
console.log('[Demo] receive', event.data);
}
});
實測結果如下:
inject script 和 content script log
謹慎處理來自 Content Script 的訊息
在 Chrome Extension 的官方文件中提到:
Content scripts are less trustworthy than the extension background page. Assume that messages from a content script might have been crafted by an attacker and make sure to validate and sanitize all input. Assume any data sent to the content script might leak to the web page. Limit the scope of privileged actions that can be triggered by messages received from content scripts.
從文中推敲,有可能 content script 和 web 共用同一份 DOM,開發者在不慎的情況下引入一些漏洞,導致駭客可以對訊息作一些手腳。因此接收來自 content script 的訊息需要作 validation,相對從 background 來的訊息就比較可靠。
以上就是我對 Chrome Extension 傳送訊息的一些整理,初次看可能還是摸不著頭緒,建議可以先實驗看看,理解之後以此文章作為對照查詢,加快後續開發的速度!
延伸閱讀
用前端框架開發 IPFS Web3 DApp 有哪些坑?
如何把 MutationObserver RxJS 化?
能夠用 Web3.js 做去中心化的 NFT Dapp 嗎?
參考資料
Message passing
Pass a message from Chrome Extension to Webpage
從零開始製作 Chrome 套件到上架商店
那些被忽略但很好用的 Web API / PostMessage