一次搞懂 Chrome Extension 訊息傳送

Chrome Extension 的 background、content script、popup script 各自跑在獨立的 js 空間中,若要傳送資料只能透過 chrome 提供的 message api。可是在不同的情境中有時可以雙向發起訊息交換,有時只能單向,對於初學者而言有點複雜。於是決定統整成這一篇,希望能夠整理各種情境,提供後續開發參考使用。我們就一起來看看吧!

Chrome Message Api

簡單來說,Chrome 提供的 Message Api 分兩種類型:

  1. 傳送訊息至 background 的 api
  2. 傳送訊息至 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

但依「傳送的來源」分成兩種:

  1. Extension 內部(如從 content script 或 popup script 發出)
  2. 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 物件發送與接收訊息!

一次搞懂 Chrome Extension 訊息傳送
來源:Google Chrome Extensions: Content Scripts and Isolated Worlds

其中需注意,在 handler 裡面要作訊息來源判斷,避免自己發送訊息後,自己的 handler 立刻收到訊息發生異常。

傳送訊息

window.postMessage(<data>);

調用 window.postMessage() 並傳入資料即可。

監聽訊息

window.addEventListener('message', (event) => {
  // do something at here...
});

在接收方用 window.addEventListener() 方法宣告 message handler,並在裡面作來源判斷。

統整一下吧!

了解 Chrome 和 Web 提供的 Message Api,我們將 發送方接收方 與使用的 api 統整成一個表格來觀察看看吧!

發送方 \ 接收方backgroundpopup scriptcontent scriptinject script
backgroundtabs.sendMessage
popup scriptruntime.sendMessagetabs.sendMessage
content scriptruntime.sendMessagewindow.postMessage
inject scriptruntime.sendMessagewindow.postMessage

從這個表格和先前討論可以確認,發送訊息所需用的 Message Api,與 接收方 是誰有關。

同時也能發現,有些 發送方 是不能發訊息給某些 接收方 的。將其真值表整理如下:

發送方 \ 接收方backgroundpopup scriptcontent scriptinject script
backgroundxvx
popup scriptvvx
content scriptvxv
inject scriptvxv

從上表格可以知道

  1. popup script 不能透過 message handler 被動接收別人發出訊息
  2. inject script 不能接收來自 background 和 popup script 發出的訊息

思考起來也合理,因為

  1. popup script 只有在使用者點 extension icon 跳出 popup 時才會啟動,因此無法在任意時刻監聽別人傳來的訊息。
  2. chrome message api 沒有提供傳送訊息至 inject script 的功能。web api 也僅能於共享 DOM 的 inject script 和 content script 之間互相溝通,因此這部份無法實現。

以下的官方示意圖也能輔佐此觀察:

一次搞懂 Chrome Extension 訊息傳送
來源:Chrome Extension Overview

程式碼範例

整理完各種情境後,我們一起來看看實際的寫法吧!

範例原始碼在此下載: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

一次搞懂 Chrome Extension 訊息傳送

content script log

一次搞懂 Chrome Extension 訊息傳送

從 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

一次搞懂 Chrome Extension 訊息傳送

傳送至 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

一次搞懂 Chrome Extension 訊息傳送

content script log

一次搞懂 Chrome Extension 訊息傳送

從 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

一次搞懂 Chrome Extension 訊息傳送

background log

一次搞懂 Chrome Extension 訊息傳送

傳送至 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

一次搞懂 Chrome Extension 訊息傳送

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

一次搞懂 Chrome Extension 訊息傳送

background log

一次搞懂 Chrome Extension 訊息傳送

傳送至 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

一次搞懂 Chrome Extension 訊息傳送

謹慎處理來自 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

Written by J
雖然大學唸的是生物,但持著興趣與熱情自學,畢業後轉戰硬體工程師,與宅宅工程師們一起過著沒日沒夜的生活,做著台灣最薄的 intel 筆電,要與 macbook air 比拼。 離開後,憑著一股傻勁與朋友創業,再度轉戰軟體工程師,一手扛起前後端、雙平台 app 開發,過程中雖跌跌撞撞,卻也累計不少經驗。 可惜不是那 1% 的成功人士,於是加入其他成功人士的新創公司,專職開發後端。沒想到卻在採前人坑的過程中,拓寬了眼界,得到了深層的領悟。