Chrome Extension 開發筆記

Kion
15 min readOct 19, 2019

--

目次

1. 範例功能簡介
2. Manifest
3. Content Script & Background Script
4. 訊息傳輸
5. 參考資料

在七月時,有做了一個密碼產生的 Chrome 套件
然後我現在才在寫,可見拖多久
不過因為當時對後端還不熟悉,所以就採用 Local storge 的方式儲存密碼
十分不安全,只能當玩具 (´_ゝ`)

會做這個套件是源於先前在決定畢展主題的時候,原先有想走資安的方向
不是技術方面的,是想往使用者端
像是資訊外洩、密碼保護等等,將這類的資訊做有效傳播
但因為組員對資安不是很了解只有恐懼就因此作罷

不過也是因此開始關注「密碼安全」這件事情
也成了我做這個套件的契機

順帶一提,目前我是使用 1Password 來管理我的所有密碼

因為這篇文會以我這支套件作為說明案例
每種套件需要的架構不同,剛好我用了全部
所以我先簡單介紹一下大概涵蓋的功能
若沒有興趣的朋友,可以直接點選 Chrome Extension 架構 閱讀

範例功能簡介

基本上我的功能只有以下幾個:

  1. 產生密碼
  2. 儲存密碼
  3. 顯示密碼
  4. 填入密碼
  5. 刪除所有密碼(為了方便 Demo 所寫)

我的介面長這樣:

功能展示

基本上我的介面都是偷懶用 Bootstrap 拉的 (〃∀〃)

透過規則選單和長度(預設 12 碼)產生符合條件的亂碼

密碼產生與儲存

我的設計是可以一直生產到自己滿意,按下 Confirm 才會存入
點選 All Password 可以看到哪個網址存了哪個密碼

對 我是用網址對應密碼的方式儲存,應該還要加帳號才對 QQ

點選 Fill in 可以直接找到填寫密碼的欄位,並找出這個網頁存的密碼填入
若先前沒有產生過或頁面上沒有可以填入密碼的欄位,會跳出提醒通知

下面是簡易的 Demo :

Demo 影片

Chrome Extension 架構

要弄懂架構,對於第一次看文件開發的我來說是一大挑戰
但也都怪自己沒有耐心,不好好看文件
我可真兜了一大圈
或許藉由敘述這個過程可以讓大家更了解整個 Chrome Extension 的架構

當然,套件可不只我所講述的內容而已,詳細可以閱讀 官方文件
還是要自己按照需求去文件探索,規劃出合適的架構
只是剛好我使用了比較常用的架構罷了

首先,我原本是完全不知道套件是可以自己寫的
我就像是發現新大陸般,對套件一無所知
我前輩也只跟我說很簡單、不難

因此,起初我以為套件是這樣的:

以為套件跟網頁是合在一起的我

什麼套件?
套件就跟寫網頁一樣,就是 HTML + CSS + JS 嘛
有什麼難的?
不就寫在網頁上?(゚⊿゚)

是的,當初的我如此天真
事後想起來,還真的不明白我怎麼會以為網頁跟套件適合在一起的?
根本沒有道理

因此我很快的發現不是這樣的,套件跟網頁是兩個不一樣的環境!
不僅如此他還有安裝檔

Manifest.json

發現環境是分開的,還有一份安裝檔的我

什麼套件?
就多一個安裝檔難的倒我?
這不就寫了嗎 (゚⊿゚)

套件也和網頁一樣,只是多了一個安裝檔而已

Manifest.json

安裝檔是最重要也是必不可少的文件,用於配置套件
除了描述套件基本資料外,還用於規範內容安全策略(CSP)、註冊腳本、管理權限…等
基本項目如下:

{
"manifest_version": 2,
"name": "My Extension",
"version": "versionString",
"browser_action": {
"default_title": ... ,
"default_icon": ... ,
"default_popup": "popup.html"
},
"content_security_policy": ... ,
"permissions" : [
...
],
"background": {
"scripts": ["background.js"],
"persistent": false
},
"content_scripts" : [ {
"matches" : [ "https://*/*", "http://localhost/*"],
"js" : [ "contentScript.js" ]
} ],
}

其他項目可以參考 官網

  1. 內容安全策略(CSP)
    Pop.html 可以引進 Pop.js ,但有個很重要的一點:
    Pop.html 不可以使用 inline JS ,因此 Pop.js 勢必要用資源的方式引入 HTML 內
    且遠端資源需要標註在 Manifest 內
    "Content_sercurity_policy" : "script-src 'self' <外部資源連結> <外部資源連結> <外部資源連結>; object-src 'self' "
  2. 管理權限(permissions)
    註冊使用的 Chrome API 以及 activeTab 可 access 的 host
  3. 註冊腳本
    註冊使用的 Background ScriptContent Script

以為套件如此爾爾的我
洋洋灑灑寫完密碼產生邏輯後,開始翻找文件找我有哪些 API 可以使用
終於選定了 localStorage 這支 API
好景不常,我遇到了 Bug,無法在 Pop.js 運行 localStorage API
於是我開始思索是不是我做錯了?
難道 Pop.js 沒辦法使用 Chrome API 嗎?

其實 Pop.js 就可以做到,是我當時菜自己沒 debug 好

不僅如此,還有我的填入功能!
網頁和套件是不同的世界
我根本獲取不了網頁的 DOM,這樣就無法找到密碼的欄位自動填字了
我到底該怎麼辦?

這時候的我意外的發現一個東西叫做 Content Script

Content Script & Background Script

發現 Content Script 的我

什麼套件?
就多一個腳本就難的倒我?
這不就寫了嗎 (゚⊿゚)

天真的我以為我無法使用 localStorage API 肯定是因為我放錯位置了
於是興高采烈的試了一下,發現我成功儲存我的密碼
但這個密碼好像不是永久的,一下就消失了

這時候我開始陷入了沈思,奇怪我明明就存了為什麼還是不成功
我試了很多方法,換了很多關鍵字查詢(就是沒用心看手上的教學文和文件)但還是找不到原因

這時候我發現了一個東西叫做 Background Script

不假思索,把 Pop.js 註冊為 Background Script

什麼套件?
就多一個腳本就難的倒我?
不知道這是在幹嘛,肯定是我把 Pop.js 放錯位置了
這不就寫了嗎 (゚⊿゚)

是的,我不假思索的把 Pop.js 註冊為 Background Script
然而沒有獲得任何幫助,我的密碼仍舊無法永久儲存
現在想起來真的是十分愚蠢的思路 (((゚Д゚;)))
當時的我在幹嘛?我是怎麼得出這個解法的?

由於當時錯誤的決定,自顧自地陷入迴圈
我躺在床上想了兩天兩夜
遊走在電腦與床鋪,絕望與更絕望之間

「不會吧 我該不會一輩子都寫不出來了吧」

對 大概就是這麼絕望

灰心喪志的我坐在電腦前不停下關鍵字
後來終於在一篇教學文發現自己的盲點
然而最悲傷的是,我早就找過這系列的教學文了,是我自己沒有認真的看手邊的資料
這兩天答案就在我身邊是我自己沒發現 ∑(ι´Дン)ノ
那時候真的很菜

那時,我才終於明白:

最終版本

尼瑪,原來他們全都獨立存在啊?

是的, Background ScriptContent Script 還有 Pop.js
是三種 Chrome Extension 不同情境會用到的 Script

  1. Pop.js :主要處理選單邏輯,只會在 彈出視窗運行 的時後才會被載入並且執行,可使用 Chrome API
  2. Background Script永久運行 的背景程式,通常用於監聽事件
    Background ScriptPop.js 功能十分相近,可使用 Chrome API,也一樣無法與當前的網頁互動,他們差就差在 生命週期的長度
    註冊為 Background Script 程式可以在背景運作,也可在 Manifest.json 註冊為非永久運行,因此被分為: persistent background pages & event pages
  3. Content Script :主要處理與 當前 網頁互動的行為,當前互動的行爲並不會與其他頁面共享,可操控頁面的 DOM 和 監聽事件,但無法使用所有 Chrome API ,註冊時須加上可 access 的 host

這時我才明白
原來我的密碼不會一直記著,是因為我放在 Content Script
而他只會儲存在當前的頁面,並不會共享啊!

因此我就按照我發現的環境,重新規劃了我的套件:

流程圖
  1. Pop.js :主要處理頁面互動、密碼產生和監聽按鈕發送訊息給 Background ScriptContent Script
  2. Background Script :主要儲存密碼和取出密碼回傳 Pop.js
  3. Content Script :主要處理定位欄位、填入密碼

才寫成了現在 Demo 的樣子

但其實我是不用寫 Background Script 的,直接寫在 Pop.js 就好
畢竟我只是存、取,沒有需要長期運行在背景頁面
但當時太蠢沒有發現

所以,重新調整後,我的架構應該是這樣:

正確版
  1. Pop.js :主要處理頁面互動、密碼產生、儲存密碼和監聽按鈕發送訊息給 Content Script
  2. Content Script :主要處理定位欄位、填入密碼

訊息傳輸

規劃好架構之後就可以開始實行了,但不妙的是:
上述講的這三種 Script 運行在不同的 Scope,要怎麼互相溝通呢?

就像我上面那張最終版的架構圖:

溝通模式

Pop.js 監聽到 Fill in 被點擊時,要如何通知 Content Script 找尋「可填入密碼的區塊」?

後來在 官網 上,我找到了訊息傳輸的方式,中間還一度誤會
會有誤會的情形是因為:Chrome API 提供了兩種傳送、一種接收方法
使三種作用域可以互相溝通:
chrome.runtime.sendMessage() & chrome.tabs.sendMessage() & chrome.runtime.onMessage.addListener()

https://developer.chrome.com/extensions/messaging

Chrome 提供了統一的接收方法,卻有兩種傳送方式
他們並不能混用,是有各自的使用時機的

chrome.runtime.sendMessage( )

用於 對 Pop.js & Background Script 傳送訊息時

chrome.runtime.sendMessage(string extension ID, any message, object options, function response Callback)
  1. string extension ID (optional) :可以向其他的擴充功能傳遞訊息,此時需附上另一個擴充功能的ID,如果省略ID,則消息只會在擴充功能內部傳送
    ID 位於 chrome://extensions/
Extension ID

2. any message:以 JSON 的形式傳送 Ex. {greeting: “hello”},通關密語的概念,以確保是想要的人接收到

3. object options (optional):TLS 通道標識符是否會傳遞至onMessageExternal 事件

4. response Callback (optional) :接收到chrome.runtime.onMessage.addListener() 回傳的值後,執行的動作

chrome.tabs.sendMessage( )

用於 對 Content Script 傳送訊息時

chrome.tabs.sendMessage(integer tabId, any message, object options, function responseCallback)
  1. integer tabId:需附上tabID作為參數,讓Chrome知道他要傳送訊息的對像是哪個內容腳本,因此不同頁籤之間並不共享內容腳本,內容腳本在每個頁籤都會單獨注入
    可透過 chrome.tabs.query取得當前tabID(tabs[0].id)
chrome.tabs.query({ active: true, currentWindow: true },    
function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, { greeting: "你好" },
function(response) {
console.log(response.farewell);
});
}
);

chrome.runtime.onMessage.addListener( )

不管是使用 chrome.runtime.sendMessage() 還是chrome.tabs.sendMessage()

接收方法皆為 chrome.runtime.onMessage.addListener()

chrome.runtime.onMessage.addListener(
function(any message, MessageSender sender, function sendResponse)
{
sendResponse({result: "ok"});
...
}
)
  1. any message:sendMessage() 傳來的 json 文字
  2. MessageSender sender:發送消息或請求的腳本對象
  3. function sendResponse:回傳 JSON 訊息給原請求對象的方法
    sendResponse({result : 'ok'}) 須盡快執行
    先前我有試過做完判斷再回傳,但都會出現: Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist. 的錯誤訊息

了解上述的傳遞機制,我的架構就更完整了!

依照原本的設計,我資料傳輸的邏輯會長這樣:

畫的有點醜

但其實我的架構應該只需要這樣:

正確版

雖然舊版和新版都可以運行
但在資訊傳遞上,新版精簡許多!

費時兩週,我終於完成了簡易的密碼產生器 。・゚・(つд`゚)・゚・
其實成就感蠻大的!
而且可以做給自己使用,很有趣
蠻鼓勵大家都玩玩看

Chrome Extension 沒有到很複雜
當時的我也還是新手
僅利用下班和週末的時間,兩週就可以做成一支簡單的套件
且經過這次的訓練,我真的比較會看文件、對英文也更有耐心
三個月後的我再回頭看文件,已經沒有當時那麼吃力的感覺了

這次的文章比較特別,是我做完成品後的三個月才開始寫文章
因為想用故事的情境帶出整體架構
我花了很多時間在回想,當時的我在想什麼
其實這個套件是我在 前輩的課 上發表的結案作業
所以有做一份簡報記錄我當時的思路
在寫文章的時候,我真的很難想像為什麼我當時是這樣解決問題的
完全想不透
且我也發現了當初寫的漏洞: 根本不用 Background Script
順手也把檔案拿出來改了一下

但我沒有刻意去修正我文章的內容,反而是記錄這個錯誤
這篇本來就是給新手看,我想我的坑也可能是他人的坑
不如把我跌跌撞撞的過程寫出來
搞不好更能破解迷思

感謝大家把這篇看完
有任何問題或是有錯誤的地方都歡迎留言
以上

拍個手讓我知道,這個文章對你們有幫助 ♥(´∀` )人
可以拍五次喔!快來瘋狂擊掌!

參考資料

  1. Chrome Extension 官方文件
  2. Chrome Extension 開發與實作系列
  3. 【乾貨】Chrome插件(擴展)開發全攻略

--

--

Kion
Kion

Written by Kion

程式就是利用自動化與排程的特性解決問題 文章分類總覽: https://hackmd.io/@Kion/SyvyEks0L

No responses yet