關於chrome extension的開發經驗總結或說明文檔等資料不少,不少人在寫,然而,我也是一員。可是,也許這篇文章,可能給你一些不同的感覺。
這裏介紹的是80%你要開發擴展會碰到的問題html
前面部分大多數是一些基礎介紹,和別人的資料大同小異,可是用的是通俗的語言或者我本身理解來描述的,不是拷貝官方的描述,否則的話,你乾脆看官方文檔就好啦,幹嗎還來我這裏折騰對吧,也許這些通俗的描述,更方便你理解(固然不排除也會有官方的話語)
後面部分多爲一些我在項目中總結的方法,這部分就是在別人的資料可能看不到的地方了,固然,這些方法也許不通用,由於畢竟是基於我項目裏的,可是儘可能總結一套方法出來。前端
廢話很少說,我們開始吧...vue
谷歌擴展(chrome extension),在認識以前,首先要明確一個觀念,這種擴展程序,實際上不是一個exe、app之類的程序,下載了本地打開運行安裝,本質上,它就是一個網頁,寫的用的都是前端的語言,高檔點說是一個程序,通俗來說, 就是運行在瀏覽器上的一個網站,網頁。jquery
我這種說法也許不對,不許確,不專業。可是起碼,能把小白開發擴展的心態,調整好點,其實是一個不難的東西,就是在寫頁面而已。要知道,心態很差,後面就堅持不下去了。web
這裏講的是開發一個擴展(插件)最經常使用最基本的所需的東西,並不像官方說的那種分類。ajax
嚴格上來說主要是background script 、 content script 和 popup,畢竟他們都是貫穿在manifest裏的,把manifest寫出來,只是爲了凸顯一下它的重要性chrome
一個插件,必須都含有這個一個文件——manifest.json,位於根目錄。顧名思義,這是一個擴展的組成清單,在這個清單裏能大約看到該插件的一個「規則」。json
羅列和簡單介紹一下一些經常使用的配置項,說以前,先看一個大體的文件,首先感官感覺一下先api
{
// 必須
"manifest_version": 2,
"name": "插件名稱a",
"version": "1.1.2",
// 推薦
"default_locale": "en",
"description": "插件的描述",
"icons": {
"16": "img/icon.png", // 擴展程序頁面上的圖標
"32": "img/icon.png", // Windows計算機一般須要此大小。提供此選項可防止尺寸失真縮小48x48選項。
"48": "img/icon.png", // 顯示在擴展程序管理頁面上
"128": "img/icon.png" // 在安裝和Chrome Webstore中顯示
},
// 可選
"background": {
"page": "background/background.html",
"scripts": ["background.js"],
// 推薦
"persistent": false
},
"browser_action": {
"default_icon": "img/icon.png",
// 特定於工具欄的圖標,至少建議使用16x16和32x32尺寸,應爲方形,
// 否則會變形
"default_title": "懸浮在工具欄插件圖標上時的tooltip內容",
"default_popup": "hello.html" // 不容許內聯JavaScript。
},
"content_scripts": [ {
"js": [ "inject.js" ],
"matches": [ "http://*/*", "https://*/*" ],
"run_at": "document_start"
} ],
"permissions": [
"contextMenus",
"tabs",
"http://*/*",
"https://*/*"
],
"web_accessible_resources": [ "dist/*", "dist/**/*" ]
}
複製代碼
上面有我寫的一些註釋,用於幫助你們更好的去理解。
那接下來開始說一下其中的配置項跨域
extension程序的圖標,能夠有一個或多個。
48x48的圖標用在extensions的管理界面(chrome://extensions);
128x128 的圖標用在安裝extension程序的時候;
16x16 的圖標看成 extension 的頁面圖標,也能夠顯示在信息欄上。
圖標通常爲PNG格式, 由於最好的透明度的支持,不過WebKit支持任何格式,包括BMP,GIF,ICO等
注意: 以上寫的圖標不是固定的。隨瀏覽器的環境的改變而變。如:安裝時彈出的對話框變小。
這兩個配置項都是用來處理擴展在瀏覽器工具欄上的表現行爲。 前者擴展能夠適用於任何頁面。後者擴展只能做用於某一頁面,當打開該頁面時觸發該Google Chrome擴展,關閉頁面則Google Chrome擴展也隨之消失。
通俗的舉個例子,一些擴展任何頁面可用,就都會顯示在工具欄上爲可用狀態,一些擴展只適用於某些頁面,如你們很熟悉的vue tools調試器,在檢測到頁面用的是vue時,就會在工具欄顯示出來並可用(非灰色)
在用戶點擊擴展程序圖標時,均可以設置彈出一個popup頁面。而這個頁面中天然是能夠有運行的js腳本的(好比就叫popup.js)。它會在每次點擊插件圖標——popup頁面彈出時,從新載入。
這個小小的設置,也就是上面我把它分爲在基本組成裏的popup了
在background裏使用一些chrome api,須要受權才能使用,例如要使用chrome.tabs.xxx的api,就要在permissions引入「tabs」
容許擴展外的頁面訪問的擴展內指定的資源。通俗來說就是,擴展是一個文件夾A的,別人的網站是一個文件夾B,B要看A的東西,須要得到權限,而寫在這個屬性下的文件,就是授予了別人訪問的權限。
background能夠理解爲插件運行在瀏覽器中的一個後臺網站/腳本,注意它是與當前瀏覽頁面無關的。
實際上這部份內容的配置狀況也會寫在manifest裏,對應的是background
配置項。單獨拿出來說,是彰顯它的份量很重,也是一個插件經常使用的配置。從其中幾個配置項項去了解一下什麼是background script
能夠理解爲這個後臺網站的主頁,在這個主頁中,有引用的腳本,其中通常都會有一個專門來管理插件各類交互以及監聽瀏覽器行爲的腳本,通常都起名爲background.js。這個主頁,不必定要求有。
這裏的腳本其實跟寫在page裏html引入的腳本目的同樣,我的的理解是,page的html在沒有的狀況下,那麼腳本就須要經過這個屬性引入了;
若是在存在page的狀況下,通常在這裏引入的腳本是專門爲插件服務的腳本,而那些第三方腳本如jquery仍是在page裏引用比較好,或許這是一個衆人的「潛規則」吧
所謂的後臺腳本,在chrome擴展中又分爲兩類,分別運行於後臺頁面(background page)和事件頁面(event page)中。二者區別在於,
前者(後臺頁面)持續運行,生存週期和瀏覽器相同,即從打開瀏覽器到關閉瀏覽器期間,後臺腳本一直在運行,一直佔據着內存等系統資源,persistent設爲true;
而後者(事件頁面)只在須要活動時活動,在徹底不活動的狀態持續幾秒後,chrome將會終止其運行,從而釋放其佔據的系統資源,而在再次有事件須要後臺腳原本處理時,從新載入它,persistent設爲false。
保持後臺腳本持久活動的惟一場合是擴展使用chrome.webRequest API來阻止或修改網絡請求。webRequest API與非持久性後臺頁面不兼容。
這部分腳本,簡單來講是插入到網頁中的腳本。它具備獨立而富有包容性。
所謂獨立,指它的工做空間,命名空間,域等是獨立的,不會說跟插入到的頁面的某些函數和變量發生衝突;
所謂包容性,指插件把本身的一些腳本(content script)插入到符合條件的頁面裏,做爲頁面的腳本,所以與插入的頁面共享dom的,即用dom操做是針對插入的網頁的,在這些腳本里使用的window對象跟插入頁面的window是同樣的。主要用在消息傳遞上(使用postMessage和onmessage)
實際上這部份內容的配置狀況也會寫在manifest裏,對應的是content_scripts
配置項。單獨拿出來說,是彰顯它的份量很重,也是一個插件經常使用的配置。從其中幾個配置項項去了解一下什麼是content script
要插入到頁面裏的腳本。例子很常見,例如在一個別人的網頁上,你要打開你作的擴展,對別人的網頁作一些處理或者獲取一些數據等,那怎麼跟別人的頁面創建起聯繫呢?就是經過把js裏的這些腳本嵌入都別人的網頁裏。
必需。匹配規則組成的數組,用來匹配頁面url的,符合條件的頁面將會插入js的腳本。固然,有能夠匹配的天然會有不匹配的——exclude_matches。匹配規則:
developer.chrome.com/extensions/…
上面的官方描述已經很清晰啦,我就很少說了。
js配置項裏的腳本什麼時候插入到頁面裏呢,這個配置項來控制插入時機。有三個選擇項:
style樣式加載好,dom渲染完成和腳本執行前
dom渲染完成後,即DOMContentLoaded後立刻執行
在DOMContentLoaded 和 window load之間,具體是什麼時刻,要視頁面的複雜程度和加載時間,並針對頁面加載速度進行了優化。
其實這部分,早就講過了,就是在manifest裏的browser_action
與page_action
配置項裏設置的
上面講述了基本的組成部分,那麼這幾部分,他們要進行交流合做,把他們組織起來,才能成就一個漂亮的擴展。那麼這種交流,分爲如下幾種說明:
最後一點,是額外說的,可是倒是很重要的。畢竟不少擴展,也是以iframe的形式呈現的。
使用
chrome.runtime.sendMessege(
message,
function(response) {…}
)
複製代碼
就能向background發送消息了,第一個參數message爲發送的消息(基礎數據類型),回調函數裏的第一個參數爲background接收消息後返回的消息(若有)
使用
chrome.runtime.onMessege.addListener(
function(request, sender, sendResponse) {…}
)
複製代碼
進行監聽發來的消息,request
表示發來的消息,sendResponse
是一個函數,用於對發來的消息進行迴應,如 sendResponse('我已收到你的消息:'+JSON.stringify(request));
這裏須要注意的是,默認狀況下sendResponse
函數的執行是同步的,若是在這個監聽消息的處理函數的同步執行流程裏沒有發現sendResponse
,則默認返回undefined
,假設咱們是要通過一個異步處理以後才調用sendResponse
,已經爲時已晚了。所以,咱們可能須要異步執行sendResponse
,這時咱們在這個監聽函數裏的添加return true
就能實現了。
還有,因爲background監聽全部頁面上的content script上發來的消息,若是多個頁面同時發送同種消息,background的onMessage只會處理最早收到的那個,其餘的不了了之了。
咱們發現,一個插件裏只有一個background環境,而content-script有多個(一個頁面一個),那麼background怎麼向特定的content-script發送消息?
首先咱們須要知道要向哪一個content scripts發送消息,通常一個頁面一份content scripts,而一個頁面對應一個瀏覽器tab,每一個tab都有本身的tabId,所以首先要獲取要發送消息的tab對應的tabId。
/** * 獲取當前選項卡id * @param callback - 獲取到id後要執行的回調函數 */
function getCurrentTabId(callback) {
chrome.tabs.query({active: true, currentWindow: true}, function (tabs) {
if (callback) {
callback(tabs.length ? tabs[0].id: null);
}
});
}
複製代碼
當知道了tabId後,就使用該api進行發送消息
chrome.tabs.sendMessage(tabId, message, function(response) {...});
複製代碼
其中message爲發送的消息,回調函數的response爲content scripts接收到消息後的回傳消息
一樣是使用
chrome.runtime.onMessege.addListener(function(request, sender, sendResponse) {…})
複製代碼
進行來自background發來消息的監聽並回傳
通常地,popup與background的交流,常見於popup要獲取background裏的某些「東西」,固然咱們可使用上述的chrome.runtime.sendMessage
和chrome.runtime.onMessage
的方式進行popup向background的交流,可是其實有更方便快捷的方式:
var bg = chrome.extension.getBackgroundPage();
bg.someMethod(); //someMethod()是background中的一個方法
複製代碼
這裏的通訊,實際上跟background與content script的方式是同樣的
其實這兩個的通訊,算不上是chrome extension開發裏的知識,它就是一個基礎的js知識——ifame與父窗體的通訊。
同域的狀況下,能夠經過DOM操做達到通訊的目的,如獲取dom元素,獲取值賦值之類的。
在父窗體裏,用window.contentWindow
獲取到iframe的window對象
在iframe裏,用window.parent
獲取到父窗體的window對象
而在跨域下,上述的方法是行不通的,網上也有各類方法解決,可是在插件這塊裏,最方便的就是使用js的message機制
了。
我這裏說的message機制
,就是使用window對象的postMessage()
和onmessage
。
通常插件展示都是在別人的網站上,所以沒辦法直接在別人的網站上添加postMessage
和onmessage
的代碼。這時候,重任就落在了插件的content script身上了(以前說了他們共用DOM)。因爲content script是本身編寫的,因此能夠「隨心所欲」了
假設iframe類名爲extension-iframe,這裏設置類名而不是id名的初衷是,咱們不能保證設置的名稱本來的網站會不會已經存在,設置類名能共存。發送消息使用
window.parent.postMessage(message, '*');
其中message
爲發送的消息
因爲一個頁面,可能有來自頁面自己的postMessage來的消息,也有可能來自該頁面其餘chrome extension發送來的消息,所以用onmessage來監聽,要作好區分來源,這裏使用如下方法
window.addEventListener('message', function (event, a, b) {
// 若是沒消息就退出
if (!event.data) {
return;
}
var iframes = document.getElementsByClassName('extension-iframe');
var extensionIframe = null; // 存插件iframe節點對象
var correctSource = false; // 是否來源正確
// 找出真正的插件生成的iframe
for (var i = 0; i < iframes.length; i++) {
if (iframes[i].contentWindow && (event.source === iframes[i].contentWindow)) {
correctSource = true;
extensionIframe = iframes[i];
break;
}
}
// 若是來源不是來自插件的,就退出
if (!correctSource) {
return;
}
}, false);
複製代碼
這裏也不能百分百區分好是否是來自本身extension的消息,或許真的那麼倒黴恰好有一個跟本身extension同類名的iframe也發了一個消息過來。所以還能夠加多一層保障,在iframe發送消息的內容上作手腳,例如加個from,而後在這邊判斷一下等。固然,這樣也不能百分百肯定,只能說保障更上一層樓了。
若是你們有好的點子,請務必告訴鄙人!受教受教!
使用 extensionIframe.contentWindow.postMessage(message, '*');
其中extensionIframe
爲插件的iframe節點對象,message
爲發送的消息,例如
{from: 'content-script', other: xxx}
複製代碼
使用
window.addEventListener('message', function (event, a, b) {
let result = event.data;
if (result && (result.from === 'content-script') && (event.source === window.parent)) {...}
});
複製代碼
在這裏,在發送消息裏增長了個from屬性,進而進一步判斷是否是來自父窗體本身插件的content script
咱們知道,在進行ajax請求,是有可能遇到跨域的。例如個人項目就是在任何一個頁面插入iframe網站,而後有些操做就須要發請求了,這樣必然存在跨域問題。
然而,若是開發插件還要開發者想辦法解決跨域問題,那chrome extension就太遜了,並且,跨不跨域,還不是瀏覽器本身的主意,是瀏覽器自己的安全策略。
因此,chrome extension爲了保證本身的優越性,容許在本身的程序裏面,實現跨域請求,那徹底的chrome extension程序,無非就是在background裏了。
所以,插件要實現一些ajax請求,都得統統搬到background裏實現。這個事情,自己不是什麼重大發現。接下來要說的是,我利用這個特性,按照某個規則,實現一套方便的請求流程。
這裏以一個ifame網站嵌入到別人頁面的這類形式的chrome extension爲例子。
首先在這個插件網站中,有一些按鈕操做自己是要觸發某些ajax請求的,可是因爲上述緣由,不能直接在插件網站裏發請求,而是先向父窗口發送消息,利用postMessage
。例如
window.parent.postMessage({
from: 'extension-iframe',
type: 'loadTable',
data: {
pageIndex: 1,
pageSize: 10,
sortProp: '',
sortOrder: 0
}
}, '*');
複製代碼
by the way,這裏用window.parent.postMessage
是爲了解決iframe跨域通訊問題,固然若是是確保同域的狀況下,其實能夠直接用DOM操做告訴父窗口一些消息。
言歸正傳,在postMessage第一個參數對象裏
屬性名 | 描述 |
---|---|
from | 標記這條消息來自哪裏 |
type | 操做的名稱,如發送該message的操做目的是爲了加載表格 |
data | 發送請求的data |
監聽發來的消息,這裏①標註的代碼爲前面說過的區分來源,這裏重點放在②部分的代碼
window.addEventListener('message', function (event, a, b) {
var responseData = event.data;
if (!event.data) {
return;
}
// 來自插件內嵌網站的消息
if (responseData.from === 'extension-iframe') {
// ① 判斷是否本身插件的iframe
var iframes = document.getElementsByClassName('extension-iframe');
var extensionIframe = null;
var correctSource = false;
for (var i = 0; i < iframes.length; i++) {
if (iframes[i].contentWindow && (event.source === iframes[i].contentWindow)) {
correctSource = true;
extensionIframe = iframes[i];
break;
}
}
if (!correctSource) {
return;
}
// ② 加載表格、提交信息等請求操做
// 該數組爲iframe傳來各個操做的名稱,對應發來的消息的type屬性
var operators = ['loadTable', 'submit', 'getNonMarkedCount', 'getUrl'];
// 若是跟操做匹配上了,就轉發給background
if (operators.indexOf(responseData.type) !== -1) {
chrome.runtime.sendMessage({
type: responseData.type,
data: responseData.data
},function (response) {
// 返回請求後的數據給iframe網站
extensionIframe.contentWindow.postMessage({
from: 'extension-content-script',
type: responseData.type,
response: response
}, '*');
});
}
}
}, false);
複製代碼
監聽剛轉發過來的消息
// 這是全部請求組成對象
var httpService = {
loadTable: function (config) {
return eodHttp.get('/brandimageservice/perspective/mark', config);
},
submit: function (config) {
return eodHttp.post('/brandimageservice/perspective/mark', config);
},
...
};
// 監聽剛轉發過來的消息
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
// 該數組爲iframe傳來各個操做的名稱,對應發來的消息的type屬性
var operators = ['loadTable', 'submit', 'getNonMarkedCount', 'getUrl'];
if (operators.indexOf(request.type) !== -1) {
// 這裏的type恰好與請求的屬性名一致
httpService[request.type](request.data).then(res => {
// 把請求的結果回傳給content script
sendResponse(res);
}).catch(e => {
// 這裏作了請求攔截,若是不是canceled的請求報錯,則把報錯信息也回傳給content script
(e.status !== -1) && (sendResponse(e.data));
});
}
// 此處return true是爲了把sendResponse做爲異步處理。
return true;
});
複製代碼
此次繞回到這個ifame裏,最終的請求的數據仍是會流回這裏。
window.addEventListener('message', function (event, a, b) {
let result = event.data;
if (result && (result.from === 'extension-content-script') && (event.source === window.parent)) {
// 如下爲請求返回內容
let res = result.response;
// 加載表格數據
if (result.type === 'loadTable') {...}
}
});
複製代碼
這樣,最終在iframe裏獲取到的請求數據仍是跟以前咱們日常開發調接口的狀況是同樣的。
整個流程是 iframe -> content script -> background -> content script -> iframe
以background爲中分線,前半截爲發送請求,後半截爲獲取請求數據。這裏巧妙的用法就是「type」這個字段由始至終都一直存在,都表明同樣的意思。這樣的寫法的好處是,全部請求操做均可以共用這麼一個流程,改一下type區分一下操做便可。
有一些chrome extension,可能不僅僅是經過點擊瀏覽器工具欄上的插件圖標來激活插件,也有一些需求是經過點擊網站上某個按鈕來激活插件(如自家的系統),那麼這時候第一步須要的是,檢測瀏覽器是否安裝了要求的chrome extension,若是沒有,進行提示等等。
在國內資料中進行搜索,每每會看到不少條教用navigator
對象來查找安裝的插件,多是我太弱雞了,我發現並不能用來檢測到本身添加的chrome extension。因而我只能另尋他法了,若是有大神知道如何用navigator
對象來判斷,麻煩指導一下。
若是安裝了某個插件,那麼該插件的content script就會插入到頁面上(沒有content script的除外,可是通常沒有content script的插件每每也沒有以上這樣的需求),所以判斷是否安裝了該插件,就變爲判斷content script是否插入到頁面上。
在content script裏寫這麼一個邏輯:往插入頁面生成一個html元素標記,如<div class="extension-flag"><div>
。而後在插入頁面獲取這個元素,若是獲取到了,就證實content script存在了(不存在也就沒有這個元素了),就證實已經安裝了。
缺點: 建立的這個元素,必定要夠「特別」,越能確保其獨一無二越能證實是來自插件的。什麼意思?假設恰好頁面也有一個類名跟建立的同樣的,那就要作進一步區分這究竟是不是來自該插件的了。
經過message機制,在合適的時機裏,頁面用postMessage
發送消息給window,content script監聽window消息,判斷若是是要求檢查是否安裝的消息,則再用postMessage
告知,只要收到這個消息,就證實已經安裝了。
缺點:因爲發送消息和接受消息再到發送消息,這個過程是異步的。因此要處理好什麼時候發送檢測的時機問題。
安裝手段通常是有兩種的,一種在谷歌商店上進行在線安裝,一種是下載安裝包離線安裝。在線安裝沒什麼好說的,那麼說一下離線安裝。
離線安裝的關鍵是,你提供的下載包是什麼?
開發者在開發擴展的時候,每每是直接安裝在本地上的擴展所在文件夾。在chrome://extensions上開啓開發者模式,點擊「加載已解壓的擴展程序」。這時候會發現第一次打開瀏覽器的時候會總是提示你這個擴展不安全之類的。固然咱們提供給用戶下載的安裝包確定不能是這個了。
一開始我傻不拉幾的直接壓縮本身的開發的擴展所在文件夾,而後發到服務器上給用戶下載,結果呢,用戶下載了,而後把壓縮包拖動到chrome://extension裏,發現chrome不容許安裝,說什麼基於安全什麼的。也就是我這個擴展可能不安全不給我裝。
後來才知道,不該該提供這種壓縮包,而是在本身發佈擴展的開發者信息中內心,把已發佈的擴展下載下來提供給用戶用才能夠。
也許...只有我那麼傻吧
最後的最後,我說一下小細節的注意項吧,稍微不留神,可能就這樣傻傻地寫下了bug了...
怎麼理解?在通訊部分我講過,在傳遞消息的時候,在消息裏,我有用type字段來標明傳遞的內容類型。在開發完擴展的時候,發現有些同事的電腦能夠正常使用有些卻不行,後來調試代碼發現,在postMessage函數裏的type參數給別的擴展改造過了,受到了影響。
爲何會這樣呢?緣由是擴展都是經過嵌入本身的腳本到別人的網頁裏,所以在一個網頁裏的代碼,特別是傳遞消息機制裏,更容易受到牽連。
這個問題說明了什麼?
對於content script的調試,日常咱們打開F12選擇到source選項的時候,通常都會顯示在"page"下,其實能夠看到,還有個content script的選擇,裏邊的就是各個擴展的內容腳本了。
對於backgroud script的調試,就在去到chrome://extensions頁下,找到對應的擴展,而後點擊背景視圖,就能夠看到backgroud script進行調試了,並且,還能在控制檯調用chrome api呢。以及,請求也能夠在這裏看到。
在擴展中,會用到不少消息傳遞,如上述的postMessage和chrome.runtime.sendMessage等之類的,你們必定要有一個觀念,他們的交流並非同步,不是說我發了一個消息過去,就立刻收到而後作接下來的處理。
因此咱們寫邏輯的時候必定要注意,這種異步性,會對你的邏輯處理產生什麼效果。特別是也要考慮到content script的插入時機是否對這些通訊產生必定影響,如content script都沒有準備好,就發了一些消息,而後就沒有聲響了。
關於chrome extensions的基本介紹和開發思路就介紹到這裏,後續會有一篇文章專門來闡述一下,我項目中遇到的需求,遇到的問題以及對應的解決方案。
感興趣的能夠關注一下,感受文章寫得對你有幫助的話,請點下贊。
轉載請標註出處謝謝,寫文章不易。