天天看點

【Chrome】678- Chrome插件開發全攻略

作者:小茗同學

本文目錄:

【Chrome】678- Chrome插件開發全攻略

​什麼是Chrome插件​

嚴格來講,我們正在說的東西應該叫Chrome擴充(​

​Chrome Extension​

​),真正意義上的Chrome插件是更底層的浏覽器功能擴充,可能需要對浏覽器源碼有一定掌握才有能力去開發。鑒于Chrome插件的叫法已經習慣,本文也全部采用這種叫法,但讀者需深知本文所描述的Chrome插件實際上指的是Chrome擴充。

Chrome插件是一個用Web技術開發、用來增強浏覽器功能的軟體,它其實就是一個由HTML、CSS、JS、圖檔等資源組成的一個.crx字尾的壓縮包.

個人猜測​

​crx​

​​可能是​

​Chrome Extension​

​如下3個字母的簡寫:

【Chrome】678- Chrome插件開發全攻略

另外,其實不隻是前端技術,Chrome插件還可以配合C++編寫的dll動态連結庫實作一些更底層的功能(NPAPI),比如全螢幕截圖。

【Chrome】678- Chrome插件開發全攻略

360搶票王插件dll截圖

由于安全原因,Chrome浏覽器42以上版本已經陸續不再支援NPAPI插件,取而代之的是更安全的PPAPI。

​學習Chrome插件開發有什麼意義​

增強浏覽器功能,輕松實作屬于自己的“定制版”浏覽器,等等。

Chrome插件提供了很多實用API供我們使用,包括但不限于:

  • 書簽控制;
  • 下載下傳控制;
  • 視窗控制;
  • 标簽控制;
  • 網絡請求控制,各類事件監聽;
  • 自定義原生菜單;
  • 完善的通信機制;
  • 等等;

​為什麼是Chrome插件而不是Firefox插件​

  1. Chrome占有率更高,更多人用;
  2. 開發更簡單;
  3. 應用場景更廣泛,Firefox插件隻能運作在Firefox上,而Chrome除了Chrome浏覽器之外,還可以運作在所有webkit核心的國産浏覽器,比如360極速浏覽器、360安全浏覽器、搜狗浏覽器、QQ浏覽器等等;
  4. 除此之外,Firefox浏覽器也對Chrome插件的運作提供了一定的支援;

​開發與調試​

Chrome插件沒有嚴格的項目結構要求,隻要保證本目錄有一個​

​manifest.json​

​即可,也不需要專門的IDE,普通的web開發工具即可。

從右上角菜單->更多工具->擴充程式可以進入 插件管理頁面,也可以直接在位址欄輸入 chrome://extensions 通路。

【Chrome】678- Chrome插件開發全攻略

勾選​

​開發者模式​

​​即可以檔案夾的形式直接加載插件,否則隻能安裝​

​.crx​

​​格式的檔案。Chrome要求插件必須從它的Chrome應用商店安裝,其它任何網站下載下傳的都無法直接安裝,是以,其實我們可以把​

​crx​

​檔案解壓,然後通過開發者模式直接加載。

開發中,代碼有任何改動都必須重新加載插件,隻需要在插件管理頁按下​

​Ctrl+R​

​即可,以防萬一最好還把頁面重新整理一下。

​核心介紹​

​manifest.json​

這是一個Chrome插件最重要也是必不可少的檔案,用來配置所有和插件相關的配置,必須放在根目錄。其中,​

​manifest_version​

​​、​

​name​

​​、​

​version​

​​3個是必不可少的,​

​description​

​​和​

​icons​

​是推薦的。

下面給出的是一些常見的配置項,均有中文注釋,完整的配置文檔請戳這裡。

{
 // 清單檔案的版本,這個必須寫,而且必須是2
 "manifest_version": 2,
 // 插件的名稱
 "name": "demo",
 // 插件的版本
 "version": "1.0.0",
 // 插件描述
 "description": "簡單的Chrome擴充demo",
 // 圖示,一般偷懶全部用一個尺寸的也沒問題
 "icons":
 {
  "16": "img/icon.png",
  "48": "img/icon.png",
  "128": "img/icon.png"
 },
 // 會一直常駐的背景JS或背景頁面
 "background":
 {
  // 2種指定方式,如果指定JS,那麼會自動生成一個背景頁
  "page": "background.html"
  //"scripts": ["js/background.js"]
 },
 // 浏覽器右上角圖示設定,browser_action、page_action、app必須三選一
 "browser_action": 
 {
  "default_icon": "img/icon.png",
  // 圖示懸停時的标題,可選
  "default_title": "這是一個示例Chrome插件",
  "default_popup": "popup.html"
 },
 // 當某些特定頁面打開才顯示的圖示
 /*"page_action":
 {
  "default_icon": "img/icon.png",
  "default_title": "我是pageAction",
  "default_popup": "popup.html"
 },*/
 // 需要直接注入頁面的JS
 "content_scripts": 
 [
  {
   //"matches": ["http://*/*", "https://*/*"],
   // "<all_urls>" 表示比對所有位址
   "matches": ["<all_urls>"],
   // 多個JS按順序注入
   "js": ["js/jquery-1.8.3.js", "js/content-script.js"],
   // JS的注入可以随便一點,但是CSS的注意就要千萬小心了,因為一不小心就可能影響全局樣式
   "css": ["css/custom.css"],
   // 代碼注入的時間,可選值: "document_start", "document_end", or "document_idle",最後一個表示頁面空閑時,預設document_idle
   "run_at": "document_start"
  },
  // 這裡僅僅是為了示範content-script可以配置多個規則
  {
   "matches": ["*://*/*.png", "*://*/*.jpg", "*://*/*.gif", "*://*/*.bmp"],
   "js": ["js/show-image-content-size.js"]
  }
 ],
 // 權限申請
 "permissions":
 [
  "contextMenus", // 右鍵菜單
  "tabs", // 标簽
  "notifications", // 通知
  "webRequest", // web請求
  "webRequestBlocking",
  "storage", // 插件本地存儲
  "http://*/*", // 可以通過executeScript或者insertCSS通路的網站
  "https://*/*" // 可以通過executeScript或者insertCSS通路的網站
 ],
 // 普通頁面能夠直接通路的插件資源清單,如果不設定是無法直接通路的
 "web_accessible_resources": ["js/inject.js"],
 // 插件首頁,這個很重要,不要浪費了這個免費廣告位
 "homepage_url": "https://www.baidu.com",
 // 覆寫浏覽器預設頁面
 "chrome_url_overrides":
 {
  // 覆寫浏覽器預設的新标簽頁
  "newtab": "newtab.html"
 },
 // Chrome40以前的插件配置頁寫法
 "options_page": "options.html",
 // Chrome40以後的插件配置頁寫法,如果2個都寫,新版Chrome隻認後面這一個
 "options_ui":
 {
  "page": "options.html",
  // 添加一些預設的樣式,推薦使用
  "chrome_style": true
 },
 // 向位址欄注冊一個關鍵字以提供搜尋建議,隻能設定一個關鍵字
 "omnibox": { "keyword" : "go" },
 // 預設語言
 "default_locale": "zh_CN",
 // devtools頁面入口,注意隻能指向一個HTML檔案,不能是JS檔案
 "devtools_page": "devtools.html"
}      

​content-scripts​

所謂content-scripts,其實就是Chrome插件中向頁面注入腳本的一種形式(雖然名為script,其實還可以包括css的),借助​

​content-scripts​

​我們可以實作通過配置的方式輕松向指定頁面注入JS和CSS(如果需要動态注入,可以參考下文),最常見的比如:廣告屏蔽、頁面CSS定制,等等。

示例配置:

{
 // 需要直接注入頁面的JS
 "content_scripts": 
 [
  {
   //"matches": ["http://*/*", "https://*/*"],
   // "<all_urls>" 表示比對所有位址
   "matches": ["<all_urls>"],
   // 多個JS按順序注入
   "js": ["js/jquery-1.8.3.js", "js/content-script.js"],
   // JS的注入可以随便一點,但是CSS的注意就要千萬小心了,因為一不小心就可能影響全局樣式
   "css": ["css/custom.css"],
   // 代碼注入的時間,可選值: "document_start", "document_end", or "document_idle",最後一個表示頁面空閑時,預設document_idle
   "run_at": "document_start"
  }
 ],
}      

特别注意,如果沒有主動指定​

​run_at​

​​為​

​document_start​

​​(預設為​

​document_idle​

​),下面這種代碼是不會生效的:

document.addEventListener('DOMContentLoaded', function()
{
 console.log('我被執行了!');
});      

​content-scripts​

​​和原始頁面共享DOM,但是不共享JS,如要通路頁面JS(例如某個JS變量),隻能通過​

​injected js​

​​來實作。​

​content-scripts​

​​不能通路絕大部分​

​chrome.xxx.api​

​,除了下面這4種:

  • chrome.extension(getURL , inIncognitoContext , lastError , onRequest , sendRequest)
  • chrome.i18n
  • chrome.runtime(connect , getManifest , getURL , id , onConnect , onMessage , sendMessage)
  • chrome.storage

其實看到這裡不要悲觀,這些API絕大部分時候都夠用了,非要調用其它API的話,你還可以通過通信來實作讓background來幫你調用(關于通信,後文有詳細介紹)。

好了,Chrome插件給我們提供了這麼強大的JS注入功能,剩下的就是發揮你的想象力去玩弄浏覽器了。

​background​

背景(姑且這麼翻譯吧),是一個常駐的頁面,它的生命周期是插件中所有類型頁面中最長的,它随着浏覽器的打開而打開,随着浏覽器的關閉而關閉,是以通常把需要一直運作的、啟動就運作的、全局的代碼放在background裡面。

background的權限非常高,幾乎可以調用所有的Chrome擴充API(除了devtools),而且它可以無限制跨域,也就是可以跨域通路任何網站而無需要求對方設定​

​CORS​

​。

經過測試,其實不止是background,所有的直接通過​

​chrome-extension://id/xx.html​

​這種方式打開的網頁都可以無限制跨域。

配置中,​

​background​

​​可以通過​

​page​

​​指定一張網頁,也可以通過​

​scripts​

​直接指定一個JS,Chrome會自動為這個JS生成一個預設的網頁:

{
 // 會一直常駐的背景JS或背景頁面
 "background":
 {
  // 2種指定方式,如果指定JS,那麼會自動生成一個背景頁
  "page": "background.html"
  //"scripts": ["js/background.js"]
 },
}      

需要特别說明的是,雖然你可以通過​

​chrome-extension://xxx/background.html​

​​直接打開背景頁,但是你打開的背景頁和真正一直在背景運作的那個頁面不是同一個,換句話說,你可以打開無數個​

​background.html​

​,但是真正在背景常駐的隻有一個,而且這個你永遠看不到它的界面,隻能調試它的代碼。

​event-pages​

這裡順帶介紹一下event-pages,它是一個什麼東西呢?鑒于background生命周期太長,長時間挂載背景可能會影響性能,是以Google又弄一個​

​event-pages​

​​,在配置檔案上,它與background的唯一差別就是多了一個​

​persistent​

​參數:

{
 "background":
 {
  "scripts": ["event-page.js"],
  "persistent": false
 },
}      

它的生命周期是:在被需要時加載,在空閑時被關閉,什麼叫被需要時呢?比如第一次安裝、插件更新、有content-script向它發送消息,等等。

除了配置檔案的變化,代碼上也有一些細微變化,個人這個簡單了解一下就行了,一般情況下background也不會很消耗性能的。

​popup​

​popup​

​​是點選​

​browser_action​

​​或者​

​page_action​

​圖示時打開的一個小視窗網頁,焦點離開網頁就立即關閉,一般用來做一些臨時性的互動。

【Chrome】678- Chrome插件開發全攻略

部落格園網摘插件popup效果

​popup​

​​可以包含任意你想要的HTML内容,并且會自适應大小。可以通過​

​default_popup​

​​字段來指定popup頁面,也可以調用​

​setPopup()​

​方法。

配置方式:

{
 "browser_action":
 {
  "default_icon": "img/icon.png",
  // 圖示懸停時的标題,可選
  "default_title": "這是一個示例Chrome插件",
  "default_popup": "popup.html"
 }
}      
【Chrome】678- Chrome插件開發全攻略

需要特别注意的是,由于單擊圖示打開popup,焦點離開又立即關閉,是以popup頁面的生命周期一般很短,需要長時間運作的代碼千萬不要寫在popup裡面。

在權限上,它和background非常類似,它們之間最大的不同是生命周期的不同,popup中可以直接通過​

​chrome.extension.getBackgroundPage()​

​擷取background的window對象。

​injected-script​

這裡的​

​injected-script​

​是我給它取的,指的是通過DOM操作的方式向頁面注入的一種JS。為什麼要把這種JS單獨拿出來讨論呢?又或者說為什麼需要通過這種方式注入JS呢?

這是因為​

​content-script​

​​有一個很大的“缺陷”,也就是無法通路頁面中的JS,雖然它可以操作DOM,但是DOM卻不能調用它,也就是無法在DOM中通過綁定事件的方式調用​

​content-script​

​​中的代碼(包括直接寫​

​onclick​

​​和​

​addEventListener​

​2種方式都不行),但是,“在頁面上添加一個按鈕并調用插件的擴充API”是一個很常見的需求,那該怎麼辦呢?其實這就是本小節要講的。

在​

​content-script​

​​中通過DOM方式向頁面注入​

​inject-script​

​代碼示例:

// 向頁面注入JS
function injectCustomJs(jsPath)
{
 jsPath = jsPath || 'js/inject.js';
 var temp = document.createElement('script');
 temp.setAttribute('type', 'text/javascript');
 // 獲得的位址類似:chrome-extension://ihcokhadfjfchaeagdoclpnjdiokfakg/js/inject.js
 temp.src = chrome.extension.getURL(jsPath);
 temp.onload = function()
 {
  // 放在頁面不好看,執行完後移除掉
  this.parentNode.removeChild(this);
 };
 document.head.appendChild(temp);
}      

你以為這樣就行了?執行一下你會看到如下報錯:

Denying load of chrome-extension://efbllncjkjiijkppagepehoekjojdclc/js/inject.js. Resources must be listed in the web_accessible_resources manifest key in order to be loaded by pages outside the extension.      

意思就是你想要在web中直接通路插件中的資源的話必須顯示聲明才行,配置檔案中增加如下:

{
 // 普通頁面能夠直接通路的插件資源清單,如果不設定是無法直接通路的
 "web_accessible_resources": ["js/inject.js"],
}      

至于​

​inject-script​

​​如何調用​

​content-script​

​中的代碼,後面我會在專門的一個消息通信章節詳細介紹。

​homepage_url​

開發者或者插件首頁設定,一般會在如下2個地方顯示:

【Chrome】678- Chrome插件開發全攻略
【Chrome】678- Chrome插件開發全攻略

​Chrome插件的8種展示形式​

​browserAction(浏覽器右上角)​

通過配置​

​browser_action​

​​可以在浏覽器的右上角增加一個圖示,一個​

​browser_action​

​​可以擁有一個圖示,一個​

​tooltip​

​​,一個​

​badge​

​​和一個​

​popup​

​。

示例配置如下:

"browser_action":
{
 "default_icon": "img/icon.png",
 "default_title": "這是一個示例Chrome插件",
 "default_popup": "popup.html"
}      

圖示

​browser_action​

​​圖示推薦使用寬高都為19像素的圖檔,更大的圖示會被縮小,格式随意,一般推薦png,可以通過manifest中​

​default_icon​

​字段配置,也可以調用setIcon()方法。

tooltip

修改​

​browser_action​

​​的manifest中​

​default_title​

​​字段,或者調用​

​setTitle()​

​方法。

【Chrome】678- Chrome插件開發全攻略

badge

所謂​

​badge​

​​就是在圖示上顯示一些文本,可以用來更新一些小的擴充狀态提示資訊。因為badge空間有限,是以隻支援4個以下的字元(英文4個,中文2個)。badge無法通過配置檔案來指定,必須通過代碼實作,設定badge文字和顔色可以分别使用​

​setBadgeText()​

​​和​

​setBadgeBackgroundColor()​

​。

chrome.browserAction.setBadgeText({text: 'new'});
chrome.browserAction.setBadgeBackgroundColor({color: [255, 0, 0, 255]});      

效果:

【Chrome】678- Chrome插件開發全攻略

​pageAction(位址欄右側)​

所謂​

​pageAction​

​​,指的是隻有當某些特定頁面打開才顯示的圖示,它和​

​browserAction​

​最大的差別是一個始終都顯示,一個隻在特定情況才顯示。

需要特别說明的是早些版本的Chrome是将pageAction放在位址欄的最右邊,左鍵單擊彈出popup,右鍵單擊則彈出相關預設的選項菜單:

【Chrome】678- Chrome插件開發全攻略

而新版的Chrome更改了這一政策,pageAction和普通的browserAction一樣也是放在浏覽器右上角,隻不過沒有點亮時是灰色的,點亮了才是彩色的,灰色時無論左鍵還是右鍵單擊都是彈出選項:

【Chrome】678- Chrome插件開發全攻略
具體是從哪一版本開始改的沒去仔細考究,反正知道v50.0的時候還是前者,v58.0的時候已改為後者。

調整之後的​

​pageAction​

​​我們可以簡單地把它看成是可以置灰的​

​browserAction​

​。

  • chrome.pageAction.show(tabId) 顯示圖示;
  • chrome.pageAction.hide(tabId) 隐藏圖示;

示例(隻有打開百度才顯示圖示):

// manifest.json
{
 "page_action":
 {
  "default_icon": "img/icon.png",
  "default_title": "我是pageAction",
  "default_popup": "popup.html"
 },
 "permissions": ["declarativeContent"]
}

// background.js
chrome.runtime.onInstalled.addListener(function(){
 chrome.declarativeContent.onPageChanged.removeRules(undefined, function(){
  chrome.declarativeContent.onPageChanged.addRules([
   {
    conditions: [
     // 隻有打開百度才顯示pageAction
     new chrome.declarativeContent.PageStateMatcher({pageUrl: {urlContains: 'baidu.com'}})
    ],
    actions: [new chrome.declarativeContent.ShowPageAction()]
   }
  ]);
 });
});      

效果圖:

【Chrome】678- Chrome插件開發全攻略

​右鍵菜單​

通過開發Chrome插件可以自定義浏覽器的右鍵菜單,主要是通過​

​chrome.contextMenus​

​API實作,右鍵菜單可以出現在不同的上下文,比如普通頁面、選中的文字、圖檔、連結,等等,如果有同一個插件裡面定義了多個菜單,Chrome會自動組合放到以插件名字命名的二級菜單裡,如下:

【Chrome】678- Chrome插件開發全攻略

最簡單的右鍵菜單示例

// manifest.json
{"permissions": ["contextMenus"]}

// background.js
chrome.contextMenus.create({
 title: "測試右鍵菜單",
 onclick: function(){alert('您點選了右鍵菜單!');}
});      

效果:

【Chrome】678- Chrome插件開發全攻略

添加右鍵百度搜尋

// manifest.json
{"permissions": ["contextMenus", "tabs"]}

// background.js
chrome.contextMenus.create({
 title: '使用度娘搜尋:%s', // %s表示選中的文字
 contexts: ['selection'], // 隻有當選中文字時才會出現此右鍵菜單
 onclick: function(params)
 {
  // 注意不能使用location.href,因為location是屬于background的window對象
  chrome.tabs.create({url: 'https://www.baidu.com/s?ie=utf-8&wd=' + encodeURI(params.selectionText)});
 }
});      

效果如下:

【Chrome】678- Chrome插件開發全攻略

文法說明

這裡隻是簡單列舉一些常用的,完整API參見:https://developer.chrome.com/extensions/contextMenus

chrome.contextMenus.create({
 type: 'normal', // 類型,可選:["normal", "checkbox", "radio", "separator"],預設 normal
 title: '菜單的名字', // 顯示的文字,除非為“separator”類型否則此參數必需,如果類型為“selection”,可以使用%s顯示標明的文本
 contexts: ['page'], // 上下文環境,可選:["all", "page", "frame", "selection", "link", "editable", "image", "video", "audio"],預設page
 onclick: function(){}, // 單擊時觸發的方法
 parentId: 1, // 右鍵菜單項的父菜單項ID。指定父菜單項将會使此菜單項成為父菜單項的子菜單
 documentUrlPatterns: 'https://*.baidu.com/*' // 隻在某些頁面顯示此右鍵菜單
});
// 删除某一個菜單項
chrome.contextMenus.remove(menuItemId);
// 删除所有自定義右鍵菜單
chrome.contextMenus.removeAll();
// 更新某一個菜單項
chrome.contextMenus.update(menuItemId, updateProperties);      

​override(覆寫特定頁面)​

使用​

​override​

​頁可以将Chrome預設的一些特定頁面替換掉,改為使用擴充提供的頁面。

擴充可以替代如下頁面:

  • 曆史記錄:從工具菜單上點選曆史記錄時通路的頁面,或者從位址欄直接輸入 chrome://history
  • 新标簽頁:當建立新标簽的時候通路的頁面,或者從位址欄直接輸入 chrome://newtab
  • 書簽:浏覽器的書簽,或者直接輸入 chrome://bookmarks

注意:

  • 一個擴充隻能替代一個頁面;
  • 不能替代隐身視窗的新标簽頁;
  • 網頁必須設定title,否則使用者可能會看到網頁的URL,造成困擾;

下面的截圖是預設的新标簽頁和被擴充替換掉的新标簽頁。

【Chrome】678- Chrome插件開發全攻略

代碼(注意,一個插件隻能替代一個預設頁,以下僅為示範):

"chrome_url_overrides":
{
 "newtab": "newtab.html",
 "history": "history.html",
 "bookmarks": "bookmarks.html"
}      

​devtools(開發者工具)​

預熱

使用過vue的應該見過這種類型的插件:

【Chrome】678- Chrome插件開發全攻略

是的,Chrome允許插件在開發者工具(devtools)上動手腳,主要表現在:

  • 自定義一個和多個和​

    ​Elements​

    ​、​

    ​Console​

    ​、​

    ​Sources​

    ​等同級别的面闆;
  • 自定義側邊欄(sidebar),目前隻能自定義​

    ​Elements​

    ​面闆的側邊欄;

先來看2張簡單的demo截圖,自定義面闆(判斷目前頁面是否使用了jQuery):

【Chrome】678- Chrome插件開發全攻略

自定義側邊欄(擷取目前頁面所有圖檔):

【Chrome】678- Chrome插件開發全攻略

devtools擴充介紹

首頁:https://developer.chrome.com/extensions/devtools

來一張官方圖檔:

【Chrome】678- Chrome插件開發全攻略

每打開一個開發者工具視窗,都會建立devtools頁面的執行個體,F12視窗關閉,頁面也随着關閉,是以devtools頁面的生命周期和devtools視窗是一緻的。devtools頁面可以通路一組特有的​

​DevTools API​

​​以及有限的擴充API,這組特有的​

​DevTools API​

​隻有devtools頁面才可以通路,background都無權通路,這些API包括:

  • ​chrome.devtools.panels​

    ​:面闆相關;
  • ​chrome.devtools.inspectedWindow​

    ​:擷取被審查視窗的有關資訊;
  • ​chrome.devtools.network​

    ​:擷取有關網絡請求的資訊;

大部分擴充API都無法直接被​

​DevTools​

​​頁面調用,但它可以像​

​content-script​

​​一樣直接調用​

​chrome.extension​

​​和​

​chrome.runtime​

​​API,同時它也可以像​

​content-script​

​一樣使用Message互動的方式與background頁面進行通信。

執行個體:建立一個devtools擴充

首先,要針對開發者工具開發插件,需要在清單檔案聲明如下:

{
 // 隻能指向一個HTML檔案,不能是JS檔案
 "devtools_page": "devtools.html"
}      

這個​

​devtools.html​

​裡面一般什麼都沒有,就引入一個js:

<!DOCTYPE html>
<html>
<head></head>
<body>
 <script type="text/javascript" src="js/devtools.js"></script>
</body>
</html>      

可以看出來,其實真正代碼是​

​devtools.js​

​​,html檔案是“多餘”的,是以這裡覺得有點坑,​

​devtools_page​

​幹嘛不允許直接指定JS呢?

再來看devtools.js的代碼:

// 建立自定義面闆,同一個插件可以建立多個自定義面闆
// 幾個參數依次為:panel标題、圖示(其實設定了也沒地方顯示)、要加載的頁面、加載成功後的回調
chrome.devtools.panels.create('MyPanel', 'img/icon.png', 'mypanel.html', function(panel)
{
 console.log('自定義面闆建立成功!'); // 注意這個log一般看不到
});

// 建立自定義側邊欄
chrome.devtools.panels.elements.createSidebarPane("Images", function(sidebar)
{
 // sidebar.setPage('../sidebar.html'); // 指定加載某個頁面
 sidebar.setExpression('document.querySelectorAll("img")', 'All Images'); // 通過表達式來指定
 //sidebar.setObject({aaa: 111, bbb: 'Hello World!'}); // 直接設定顯示某個對象
});      

setPage時的效果:

【Chrome】678- Chrome插件開發全攻略

以下截圖示例的代碼:

【Chrome】678- Chrome插件開發全攻略
// 檢測jQuery
document.getElementById('check_jquery').addEventListener('click', function()
{
 // 通路被檢查的頁面DOM需要使用inspectedWindow
 // 簡單例子:檢測被檢查頁面是否使用了jQuery
 chrome.devtools.inspectedWindow.eval("jQuery.fn.jquery", function(result, isException)
 {
  var html = '';
  if (isException) html = '目前頁面沒有使用jQuery。';
  else html = '目前頁面使用了jQuery,版本為:'+result;
  alert(html);
 });
});

// 打開某個資源
document.getElementById('open_resource').addEventListener('click', function()
{
 chrome.devtools.inspectedWindow.eval("window.location.href", function(result, isException)
 {
  chrome.devtools.panels.openResource(result, 20, function()
  {
   console.log('資源打開成功!');
  });
 });
});

// 審查元素
document.getElementById('test_inspect').addEventListener('click', function()
{
 chrome.devtools.inspectedWindow.eval("inspect(document.images[0])", function(result, isException){});
});

// 擷取所有資源
document.getElementById('get_all_resources').addEventListener('click', function()
{
 chrome.devtools.inspectedWindow.getResources(function(resources)
 {
  alert(JSON.stringify(resources));
 });
});      

調試技巧

修改了devtools頁面的代碼時,需要先在 chrome://extensions 頁面按下​

​Ctrl+R​

​重新加載插件,然後關閉再打開開發者工具即可,無需重新整理頁面(而且隻重新整理頁面不重新整理開發者工具的話是不會生效的)。

由于devtools本身就是開發者工具頁面,是以幾乎沒有方法可以直接調試它,直接用 ​

​chrome-extension://extid/devtools.html"​

​的方式打開頁面肯定報錯,因為不支援相關特殊API,隻能先自己寫一些方法屏蔽這些錯誤,調試通了再放開。

​option(選項頁)​

所謂​

​options​

​頁,就是插件的設定頁面,有2個入口,一個是右鍵圖示有一個“選項”菜單,還有一個在插件管理頁面:

【Chrome】678- Chrome插件開發全攻略
【Chrome】678- Chrome插件開發全攻略

在Chrome40以前,options頁面和其它普通頁面沒什麼差別,Chrome40以後則有了一些變化。

我們先看老版的options:

{
 // Chrome40以前的插件配置頁寫法
 "options_page": "options.html",
}      

這個頁面裡面的内容就随你自己發揮了,配置之後在插件管理頁就會看到一個​

​選項​

​按鈕入口,點進去就是打開一個網頁,沒啥好講的。

效果:

【Chrome】678- Chrome插件開發全攻略

再來看新版的optionsV2:

{
 "options_ui":
 {
     "page": "options.html",
  // 添加一些預設的樣式,推薦使用
     "chrome_style": true
 },
}      

​options.html​

​的代碼我們沒有任何改動,隻是配置檔案改了,之後效果如下:

【Chrome】678- Chrome插件開發全攻略

看起來是不是高大上了?

幾點注意:

  • 為了相容,建議2種都寫,如果都寫了,Chrome40以後會預設讀取新版的方式;
  • 新版options中不能使用alert;
  • 資料存儲建議用chrome.storage,因為會随使用者自動同步;

​omnibox​

​omnibox​

​​是向使用者提供搜尋建議的一種方式。先來看個​

​gif​

​圖以便了解一下這東西到底是個什麼鬼:

【Chrome】678- Chrome插件開發全攻略

注冊某個關鍵字以觸發插件自己的搜尋建議界面,然後可以任意發揮了。

首先,配置檔案如下:

{
 // 向位址欄注冊一個關鍵字以提供搜尋建議,隻能設定一個關鍵字
 "omnibox": { "keyword" : "go" },
}      

然後​

​background.js​

​中注冊監聽事件:

// omnibox 示範
chrome.omnibox.onInputChanged.addListener((text, suggest) => {
 console.log('inputChanged: ' + text);
 if(!text) return;
 if(text == '美女') {
  suggest([
   {content: '中國' + text, description: '你要找“中國美女”嗎?'},
   {content: '日本' + text, description: '你要找“日本美女”嗎?'},
   {content: '泰國' + text, description: '你要找“泰國美女或人妖”嗎?'},
   {content: '南韓' + text, description: '你要找“南韓美女”嗎?'}
  ]);
 }
 else if(text == '微網誌') {
  suggest([
   {content: '新浪' + text, description: '新浪' + text},
   {content: '騰訊' + text, description: '騰訊' + text},
   {content: '搜狐' + text, description: '搜尋' + text},
  ]);
 }
 else {
  suggest([
   {content: '百度搜尋 ' + text, description: '百度搜尋 ' + text},
   {content: '谷歌搜尋 ' + text, description: '谷歌搜尋 ' + text},
  ]);
 }
});

// 當使用者接收關鍵字建議時觸發
chrome.omnibox.onInputEntered.addListener((text) => {
    console.log('inputEntered: ' + text);
 if(!text) return;
 var href = '';
    if(text.endsWith('美女')) href = 'http://image.baidu.com/search/index?tn=baiduimage&ie=utf-8&word=' + text;
 else if(text.startsWith('百度搜尋')) href = 'https://www.baidu.com/s?ie=UTF-8&wd=' + text.replace('百度搜尋 ', '');
 else if(text.startsWith('谷歌搜尋')) href = 'https://www.google.com.tw/search?q=' + text.replace('谷歌搜尋 ', '');
 else href = 'https://www.baidu.com/s?ie=UTF-8&wd=' + text;
 openUrlCurrentTab(href);
});
// 擷取目前頁籤ID
function getCurrentTabId(callback)
{
 chrome.tabs.query({active: true, currentWindow: true}, function(tabs)
 {
  if(callback) callback(tabs.length ? tabs[0].id: null);
 });
}

// 目前标簽打開某個連結
function openUrlCurrentTab(url)
{
 getCurrentTabId(tabId => {
  chrome.tabs.update(tabId, {url: url});
 })
}      

​桌面通知​

Chrome提供了一個​

​chrome.notifications​

​​API以便插件推送桌面通知,暫未找到​

​chrome.notifications​

​​和HTML5自帶的​

​Notification​

​的顯著差別及優勢。

在背景JS中,無論是使用​

​chrome.notifications​

​​還是​

​Notification​

​都不需要申請權限(HTML5方式需要申請權限),直接使用即可。

最簡單的通知:

【Chrome】678- Chrome插件開發全攻略

代碼:

chrome.notifications.create(null, {
 type: 'basic',
 iconUrl: 'img/icon.png',
 title: '這是标題',
 message: '您剛才點選了自定義右鍵菜單!'
});      

通知的樣式可以很豐富:

【Chrome】678- Chrome插件開發全攻略

這個沒有深入研究,有需要的可以去看官方文檔。

​5種類型的JS對比​

Chrome插件的JS主要可以分為這5類:​

​injected script​

​​、​

​content-script​

​​、​

​popup js​

​​、​

​background js​

​​和​

​devtools js​

​,

​權限對比​

JS種類 可通路的API DOM通路情況 JS通路情況 直接跨域
injected script 和普通JS無任何差别,不能通路任何擴充API 可以通路 可以通路 不可以
content script 隻能通路 extension、runtime等部分API 可以通路 不可以 不可以
popup js 可通路絕大部分API,除了devtools系列 不可直接通路 不可以 可以
background js 可通路絕大部分API,除了devtools系列 不可直接通路 不可以 可以
devtools js 隻能通路 devtools、extension、runtime等部分API 可以 可以 不可以

​調試方式對比​

JS類型 調試方式 圖檔說明
injected script 直接普通的F12即可 懶得截圖
content-script 打開Console,如圖切換
【Chrome】678- Chrome插件開發全攻略
popup-js popup頁面右鍵審查元素
【Chrome】678- Chrome插件開發全攻略
background 插件管理頁點選背景頁即可
【Chrome】678- Chrome插件開發全攻略
devtools-js 暫未找到有效方法 -

​消息通信​

通信首頁:https://developer.chrome.com/extensions/messaging

前面我們介紹了Chrome插件中存在的5種JS,那麼它們之間如何互相通信呢?下面先來系統概況一下,然後再分類細說。需要知道的是,popup和background其實幾乎可以視為一種東西,因為它們可通路的API都一樣、通信機制一樣、都可以跨域。

​互相通信概覽​

注:​

​-​

​表示不存在或者無意義,或者待驗證。

injected-script content-script popup-js background-js
injected-script - window.postMessage - -
content-script window.postMessage - chrome.runtime.sendMessage chrome.runtime.connect chrome.runtime.sendMessage chrome.runtime.connect
popup-js - chrome.tabs.sendMessage chrome.tabs.connect - chrome.extension. getBackgroundPage()
background-js - chrome.tabs.sendMessage chrome.tabs.connect chrome.extension.getViews -
devtools-js chrome.devtools. inspectedWindow.eval - chrome.runtime.sendMessage chrome.runtime.sendMessage

​通信詳細介紹​

popup和background

popup可以直接調用background中的JS方法,也可以直接通路background的DOM:

// background.js
function test()
{
 alert('我是background!');
}

// popup.js
var bg = chrome.extension.getBackgroundPage();
bg.test(); // 通路bg的函數
alert(bg.document.body.innerHTML); // 通路bg的DOM      
小插曲,今天碰到一個情況,發現popup無法擷取background的任何方法,找了半天才發現是因為background的js報錯了,而你如果不主動檢視background的js的話,是看不到錯誤資訊的,特此提醒。

至于​

​background​

​​通路​

​popup​

​​如下(前提是​

​popup​

​已經打開):

var views = chrome.extension.getViews({type:'popup'});
if(views.length > 0) {
 console.log(views[0].location.href);
}      

popup或者bg向content主動發送消息

background.js或者popup.js:

function sendMessageToContentScript(message, callback)
{
 chrome.tabs.query({active: true, currentWindow: true}, function(tabs)
 {
  chrome.tabs.sendMessage(tabs[0].id, message, function(response)
  {
   if(callback) callback(response);
  });
 });
}
sendMessageToContentScript({cmd:'test', value:'你好,我是popup!'}, function(response)
{
 console.log('來自content的回複:'+response);
});      

​content-script.js​

​接收:

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse)
{
 // console.log(sender.tab ?"from a content script:" + sender.tab.url :"from the extension");
 if(request.cmd == 'test') alert(request.value);
 sendResponse('我收到了你的消息!');
});      

雙方通信直接發送的都是JSON對象,不是JSON字元串,是以無需解析,很友善(當然也可以直接發送字元串)。

網上有些老代碼中用的是​

​chrome.extension.onMessage​

​​,沒有完全查清二者的差別(貌似是别名),但是建議統一使用​

​chrome.runtime.onMessage​

​。

content-script主動發消息給背景

content-script.js:

chrome.runtime.sendMessage({greeting: '你好,我是content-script呀,我主動發消息給背景!'}, function(response) {
 console.log('收到來自背景的回複:' + response);
});      

background.js 或者 popup.js:

// 監聽來自content-script的消息
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse)
{
 console.log('收到來自content-script的消息:');
 console.log(request, sender, sendResponse);
 sendResponse('我是背景,我已收到你的消息:' + JSON.stringify(request));
});      

注意事項:

  • content_scripts向​

    ​popup​

    ​主動發消息的前提是popup必須打開!否則需要利用background作中轉;
  • 如果background和popup同時監聽,那麼它們都可以同時收到消息,但是隻有一個可以sendResponse,一個先發送了,那麼另外一個再發送就無效;

injected script和content-script

​content-script​

​​和頁面内的腳本(​

​injected-script​

​自然也屬于頁面内的腳本)之間唯一共享的東西就是頁面的DOM元素,有2種方法可以實作二者通訊:

  1. 可以通過​

    ​window.postMessage​

    ​和​

    ​window.addEventListener​

    ​來實作二者消息通訊;
  2. 通過自定義DOM事件來實作;

第一種方法(推薦):

​injected-script​

​中:

window.postMessage({"test": '你好!'}, '*');      

content script中:

window.addEventListener("message", function(e)
{
 console.log(e.data);
}, false);      

第二種方法:

​injected-script​

​中:

var customEvent = document.createEvent('Event');
customEvent.initEvent('myCustomEvent', true, true);
function fireCustomEvent(data) {
 hiddenDiv = document.getElementById('myCustomEventDiv');
 hiddenDiv.innerText = data
 hiddenDiv.dispatchEvent(customEvent);
}
fireCustomEvent('你好,我是普通JS!');      

​content-script.js​

​中:

var hiddenDiv = document.getElementById('myCustomEventDiv');
if(!hiddenDiv) {
 hiddenDiv = document.createElement('div');
 hiddenDiv.style.display = 'none';
 document.body.appendChild(hiddenDiv);
}
hiddenDiv.addEventListener('myCustomEvent', function() {
 var eventData = document.getElementById('myCustomEventDiv').innerText;
 console.log('收到自定義事件消息:' + eventData);
});      

​長連接配接和短連接配接​

其實上面已經涉及到了,這裡再單獨說明一下。Chrome插件中有2種通信方式,一個是短連接配接(​

​chrome.tabs.sendMessage​

​​和​

​chrome.runtime.sendMessage​

​​),一個是長連接配接(​

​chrome.tabs.connect​

​​和​

​chrome.runtime.connect​

​)。

短連接配接的話就是擠牙膏一樣,我發送一下,你收到了再回複一下,如果對方不回複,你隻能重新發,而長連接配接類似​

​WebSocket​

​會一直建立連接配接,雙方可以随時互發消息。

短連接配接上面已經有代碼示例了,這裡隻講一下長連接配接。

popup.js:

getCurrentTabId((tabId) => {
 var port = chrome.tabs.connect(tabId, {name: 'test-connect'});
 port.postMessage({question: '你是誰啊?'});
 port.onMessage.addListener(function(msg) {
  alert('收到消息:'+msg.answer);
  if(msg.answer && msg.answer.startsWith('我是'))
  {
   port.postMessage({question: '哦,原來是你啊!'});
  }
 });
});      

content-script.js:

// 監聽長連接配接
chrome.runtime.onConnect.addListener(function(port) {
 console.log(port);
 if(port.name == 'test-connect') {
  port.onMessage.addListener(function(msg) {
   console.log('收到長連接配接消息:', msg);
   if(msg.question == '你是誰啊?') port.postMessage({answer: '我是你爸!'});
  });
 }
});      

​其它補充​

​動态注入或執行JS​

雖然在​

​background​

​​和​

​popup​

​​中無法直接通路頁面DOM,但是可以通過​

​chrome.tabs.executeScript​

​來執行腳本,進而實作通路web頁面的DOM(注意,這種方式也不能直接通路頁面JS)。

示例​

​manifest.json​

​配置:

{
 "name": "動态JS注入示範",
 ...
 "permissions": [
  "tabs", "http://*/*", "https://*/*"
 ],
 ...
}      

JS:

// 動态執行JS代碼
chrome.tabs.executeScript(tabId, {code: 'document.body.style.backgroundColor="red"'});
// 動态執行JS檔案
chrome.tabs.executeScript(tabId, {file: 'some-script.js'});      

​動态注入CSS​

示例​

​manifest.json​

​配置:

{
 "name": "動态CSS注入示範",
 ...
 "permissions": [
  "tabs", "http://*/*", "https://*/*"
 ],
 ...
}      

JS代碼:

// 動态執行CSS代碼,TODO,這裡有待驗證
chrome.tabs.insertCSS(tabId, {code: 'xxx'});
// 動态執行CSS檔案
chrome.tabs.insertCSS(tabId, {file: 'some-style.css'});      

​擷取目前視窗ID​

chrome.windows.getCurrent(function(currentWindow)
{
 console.log('目前視窗ID:' + currentWindow.id);
});      

​擷取目前标簽頁ID​

一般有2種方法:

// 擷取目前頁籤ID
function getCurrentTabId(callback)
{
 chrome.tabs.query({active: true, currentWindow: true}, function(tabs)
 {
  if(callback) callback(tabs.length ? tabs[0].id: null);
 });
}      

擷取目前頁籤id的另一種方法,大部分時候都類似,隻有少部分時候會不一樣(例如當視窗最小化時)

// 擷取目前頁籤ID
function getCurrentTabId2()
{
 chrome.windows.getCurrent(function(currentWindow)
 {
  chrome.tabs.query({active: true, windowId: currentWindow.id}, function(tabs)
  {
   if(callback) callback(tabs.length ? tabs[0].id: null);
  });
 });
}      

​本地存儲​

本地存儲建議用​

​chrome.storage​

​​而不是普通的​

​localStorage​

​,差別有好幾點,個人認為最重要的2點差別是:

  • ​chrome.storage​

    ​​是針對插件全局的,即使你在​

    ​background​

    ​中儲存的資料,在​

    ​content-script​

    ​也能擷取到;
  • ​chrome.storage.sync​

    ​可以跟随目前登入使用者自動同步,這台電腦修改的設定會自動同步到其它電腦,很友善,如果沒有登入或者未聯網則先儲存到本地,等登入了再同步至網絡;

需要聲明​

​storage​

​​權限,有​

​chrome.storage.sync​

​​和​

​chrome.storage.local​

​2種方式可供選擇,使用示例如下:

// 讀取資料,第一個參數是指定要讀取的key以及設定預設值
chrome.storage.sync.get({color: 'red', age: 18}, function(items) {
 console.log(items.color, items.age);
});
// 儲存資料
chrome.storage.sync.set({color: 'blue'}, function() {
 console.log('儲存成功!');
});      

​webRequest​

通過webRequest系列API可以對HTTP請求進行任性地修改、定制,這裡通過​

​beforeRequest​

​來簡單示範一下它的冰山一角:

//manifest.json
{
 // 權限申請
 "permissions":
 [
  "webRequest", // web請求
  "webRequestBlocking", // 阻塞式web請求
  "storage", // 插件本地存儲
  "http://*/*", // 可以通過executeScript或者insertCSS通路的網站
  "https://*/*" // 可以通過executeScript或者insertCSS通路的網站
 ],
}


// background.js
// 是否顯示圖檔
var showImage;
chrome.storage.sync.get({showImage: true}, function(items) {
 showImage = items.showImage;
});
// web請求監聽,最後一個參數表示阻塞式,需單獨聲明權限:webRequestBlocking
chrome.webRequest.onBeforeRequest.addListener(details => {
 // cancel 表示取消本次請求
 if(!showImage && details.type == 'image') return {cancel: true};
 // 簡單的音視訊檢測
 // 大部分網站視訊的type并不是media,且視訊做了防下載下傳處理,是以這裡僅僅是為了示範效果,無實際意義
 if(details.type == 'media') {
  chrome.notifications.create(null, {
   type: 'basic',
   iconUrl: 'img/icon.png',
   title: '檢測到音視訊',
   message: '音視訊位址:' + details.url,
  });
 }
}, {urls: ["<all_urls>"]}, ["blocking"]);      

​國際化​

插件根目錄建立一個名為​

​_locales​

​​的檔案夾,再在下面建立一些語言的檔案夾,如​

​en​

​​、​

​zh_CN​

​​、​

​zh_TW​

​​,然後再在每個檔案夾放入一個​

​messages.json​

​​,同時必須在清單檔案中設定​

​default_locale​

​。

​_locales\en\messages.json​

​内容:

{
 "pluginDesc": {"message": "A simple chrome extension demo"},
 "helloWorld": {"message": "Hello World!"}
}      

​_locales\zh_CN\messages.json​

​内容:

{
 "pluginDesc": {"message": "一個簡單的Chrome插件demo"},
 "helloWorld": {"message": "你好啊,世界!"}
}      

在​

​manifest.json​

​​和​

​CSS​

​​檔案中通過​

​__MSG_messagename__​

​引入,如:

{
 "description": "__MSG_pluginDesc__",
 // 預設語言
 "default_locale": "zh_CN",
}      

JS中則直接​

​chrome.i18n.getMessage("helloWorld")​

​。

測試時,通過給chrome建立一個不同的快捷方式​

​chrome.exe --lang=en​

​來切換語言,如:

【Chrome】678- Chrome插件開發全攻略

英文效果:

【Chrome】678- Chrome插件開發全攻略

中文效果:

【Chrome】678- Chrome插件開發全攻略

​API總結​

比較常用用的一些API系列:

  • chrome.tabs
  • chrome.runtime
  • chrome.webRequest
  • chrome.window
  • chrome.storage
  • chrome.contextMenus
  • chrome.devtools
  • chrome.extension

​經驗總結​

​檢視已安裝插件路徑​

已安裝的插件源碼路徑:​

​C:\Users\使用者名\AppData\Local\Google\Chrome\User Data\Default\Extensions​

​,每一個插件被放在以插件ID為名的檔案夾裡面,想要學習某個插件的某個功能是如何實作的,看人家的源碼是最好的方法了:

【Chrome】678- Chrome插件開發全攻略

如何檢視某個插件的ID?進入 chrome://extensions ,然後勾線開發者模式即可看到了。

【Chrome】678- Chrome插件開發全攻略

​特别注意background的報錯​

很多時候你發現你的代碼會莫名其妙的失效,找來找去又找不到原因,這時打開background的控制台才發現原來某個地方寫錯了導緻代碼沒生效,正式由于background報錯的隐蔽性(需要主動打開對應的控制台才能看到錯誤),是以特别注意這點。

​如何讓popup頁面不關閉​

在對popup頁面審查元素的時候popup會被強制打開無法關閉,隻有控制台關閉了才可以關閉popup,原因很簡單:如果popup關閉了控制台就沒用了。這種方法在某些情況下很實用!

​不支援内聯JavaScript的執行​

也就是不支援将js直接寫在html中,比如:

<input id="btn" type="button" value="收藏" onclick="test()"/>      

報錯如下:

Refused to execute inline event handler because it violates the following Content Security Policy directive: "script-src 'self' blob: filesystem: chrome-extension-resource:". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution.      

解決方法就是用JS綁定事件:

$('#btn').on('click', function(){alert('測試')});      

另外,對于A标簽,這樣寫​

​href="javascript:;"​

​​然後用JS綁定事件雖然控制台會報錯,但是不受影響,當然強迫症患者受不了的話隻能寫成​

​href="#"​

​了。

如果這樣寫:

<a href="javascript:;" id="get_secret">請求secret</a>      

報錯如下:

Refused to execute JavaScript URL because it violates the following Content Security Policy directive: "script-src 'self' blob: filesystem: chrome-extension-resource:". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution.      

​注入CSS的時候必須小心​

由于通過​

​content_scripts​

​注入的CSS優先級非常高,幾乎僅次于浏覽器預設樣式,稍不注意可能就會影響一些網站的展示效果,是以盡量不要寫一些影響全局的樣式。

之是以強調這個,是因為這個帶來的問題非常隐蔽,不太容易找到,可能你正在寫某個網頁,昨天樣式還是好好的,怎麼今天就突然不行了?然後你辛辛苦苦找來找去,找了半天才發現竟然是因為插件裡面的一個樣式影響的!

【Chrome】678- Chrome插件開發全攻略

​打包與釋出​

打包的話直接在插件管理頁有一個打包按鈕:

【Chrome】678- Chrome插件開發全攻略

然後會生成一個​

​.crx​

​檔案,要釋出到Google應用商店的話需要先登入你的Google賬号,然後花5個$注冊為開發者,本人太窮,就懶得親自驗證了,有釋出需求的自己去整吧。

【Chrome】678- Chrome插件開發全攻略

​參考​

​官方資料​

推薦檢視官方文檔,雖然是英文,但是全且新,國内的中文資料都比較舊(注意以下全部需要翻牆):

  • Chrome插件官方文檔首頁
  • Chrome插件官方示例
  • manifest清單檔案
  • permissions權限
  • chrome.xxx.api文檔
  • 模糊比對規則文法詳解

​第三方資料​

部分中文資料,不是特别推薦:

  • 360安全浏覽器開發文檔
  • 360極速浏覽器Chrome擴充開發文檔
  • Chrome擴充開發極客系列部落格