最近有著想要開發 LikeCoin Wallet 擴充插件的想法,類似於 MetaMask 的做法,將一個物件,例如 likecoin
提供到 window
下,這樣網頁就可以透過 likecoin.transfer
來轉帳。這篇文紀錄可能如何達成。
三種 Script
首先,先介紹 Web Extension 中三個主要的 Script。第一是 Background Script,顧名思義就是在後台常駐。第二種是 Content Script,這個則會在指定的頁面中執行,第三種是 Page Script,指網頁中的腳本。
其中,Background Script 和 Content Script 都不會受到跨域請求、CSP 的限制,給了非常大的操作空間。
而由於 Content Script 是和 Page Script 分開執行,因此直接在 Content Script 寫 window['likecoin'] = {}
是行不通的。(相關的概念似乎是叫做 Xray。)
因此,接下來的問題就成了如何讓網頁可以取用我們訂定的物件,並讓這個物件可以享有 Content Script 的優勢。
將物件提供給 Window
在許多相關的討論中,會開一個 inpage.js
,並透過在 manifest.json
中加入 web_accessible_resources
。如此一來,網頁就可以通過 <script src="extension://.../inpage.js">
的形式來載入 inpage.js
。此時,inpage.js
則成為 Page Script 的一部分,因此就可以被網站呼叫。
透過 Content Script 發起跨域請求
然而,既然成為 Page Script,那就無法享有前面提到的跨域請求和 CSP 的優勢。這時,我們可以透過 postMessage
與 message
事件,將 inpage.js
中的請求轉交給 Content Script,Content Script 發起請求並獲得回應時再用相同方式回傳給 inpage.js
。
inpage.js:
const IDENTIFIER = 'demo-extension'
window.postMessage({
url: 'https://example.com',
_identifier: IDENTIFIER,
}, window.location.origin)
window.addEventListener('message', event => {
if (!event.data || event.data._identifier !== IDENTIFIER) return
const res = event.data.body
// 使用回傳的內容
})
Content Script:
const IDENTIFIER = 'demo-extension'
window.addEventListener('message', event => {
if (!event.data || event.data._identifier !== IDENTIFIER) return
fetch(event.data.url).then(res => res.text()).then(data => {
window.postMessage({
body: data,
_identifier: IDENTIFIER,
}, window.location.origin)
})
})
window.postMessage({
url: 'https://example.com',
_identifier: IDENTIFIER,
}, window.location.origin)
由於 Message 是對整個 window 發出,為了避免處理到來自其他 extension 或網頁本身的 Message,上面加入了 identifier。並限制 origin 來避免其他意外的操作,例如通過 iframe 的模擬。
然而必須留意的是網站也可以模擬插件的事件送出 message,因此若是隱私資料或資料更動,可能需要額外通過 popup 或其他手段來驗證。例如若收到轉帳的 message 而沒驗證的話,就有被盜取餘額的風險。