谷歌瀏覽器插件開發(轉發:https://www.cnblogs.com/liuxianan/p/chrome-plugin-develop.html#!comments)

看到一篇很棒的適合剛接觸的人員,特此轉載一下,慢慢學習javascript

 

 

【乾貨】Chrome插件(擴展)開發全攻略

 

寫在前面

我花了將近一個多月的時間斷斷續續寫下這篇博文,並精心寫下完整demo,寫博客的辛苦你們懂的,因此轉載務必保留出處。本文全部涉及到的大部分代碼均在這個demo裏面:https://github.com/sxei/chrome-plugin-demo ,你們能夠直接下載下來運行。css

另外,本文圖片較多,且圖片服務器帶寬有限,右下角的目錄滾動監聽必須等到圖片所有加載完畢以後纔會觸發,因此請耐心等待加載完畢。html

本文目錄:前端

demo部分截圖:vue

前言

2.1. 什麼是Chrome插件

嚴格來說,咱們正在說的東西應該叫Chrome擴展(Chrome Extension),真正意義上的Chrome插件是更底層的瀏覽器功能擴展,可能須要對瀏覽器源碼有必定掌握纔有能力去開發。鑑於Chrome插件的叫法已經習慣,本文也所有采用這種叫法,但讀者需深知本文所描述的Chrome插件實際上指的是Chrome擴展。java

Chrome插件是一個用Web技術開發、用來加強瀏覽器功能的軟件,它其實就是一個由HTML、CSS、JS、圖片等資源組成的一個.crx後綴的壓縮包.jquery

我的猜想crx多是Chrome Extension以下3個字母的簡寫:git

另外,其實不僅是前端技術,Chrome插件還能夠配合C++編寫的dll動態連接庫實現一些更底層的功能(NPAPI),好比全屏幕截圖。github

因爲安全緣由,Chrome瀏覽器42以上版本已經陸續再也不支持NPAPI插件,取而代之的是更安全的PPAPI。web

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

加強瀏覽器功能,輕鬆實現屬於本身的「定製版」瀏覽器,等等。

Chrome插件提供了不少實用API供咱們使用,包括但不限於:

  • 書籤控制;
  • 下載控制;
  • 窗口控制;
  • 標籤控制;
  • 網絡請求控制,各種事件監聽;
  • 自定義原生菜單;
  • 完善的通訊機制;
  • 等等;

2.3. 爲何是Chrome插件而不是Firefox插件

  1. Chrome佔有率更高,更多人用;
  2. 開發更簡單;
  3. 應用場景更普遍,Firefox插件只能運行在Firefox上,而Chrome除了Chrome瀏覽器以外,還能夠運行在全部webkit內核的國產瀏覽器,好比360極速瀏覽器、360安全瀏覽器、搜狗瀏覽器、QQ瀏覽器等等;
  4. 除此以外,Firefox瀏覽器也對Chrome插件的運行提供了必定的支持;

開發與調試

Chrome插件沒有嚴格的項目結構要求,只要保證本目錄有一個manifest.json便可,也不須要專門的IDE,普通的web開發工具便可。

從右上角菜單->更多工具->擴展程序能夠進入 插件管理頁面,也能夠直接在地址欄輸入 chrome://extensions 訪問。

勾選開發者模式便可以文件夾的形式直接加載插件,不然只能安裝.crx格式的文件。Chrome要求插件必須從它的Chrome應用商店安裝,其它任何網站下載的都沒法直接安裝,因此,其實咱們能夠把crx文件解壓,而後經過開發者模式直接加載。

開發中,代碼有任何改動都必須從新加載插件,只須要在插件管理頁按下Ctrl+R便可,以防萬一最好還把頁面刷新一下。

核心介紹

4.1. manifest.json

這是一個Chrome插件最重要也是必不可少的文件,用來配置全部和插件相關的配置,必須放在根目錄。其中,manifest_versionnameversion3個是必不可少的,descriptionicons是推薦的。

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

{
    // 清單文件的版本,這個必須寫,並且必須是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" }

4.2. 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_atdocument_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注入功能,剩下的就是發揮你的想象力去玩弄瀏覽器了。

4.3. 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,可是真正在後臺常駐的只有一個,並且這個你永遠看不到它的界面,只能調試它的代碼。

4.4. event-pages

這裏順帶介紹一下event-pages,它是一個什麼東西呢?鑑於background生命週期太長,長時間掛載後臺可能會影響性能,因此Google又弄一個event-pages,在配置文件上,它與background的惟一區別就是多了一個persistent參數:

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

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

除了配置文件的變化,代碼上也有一些細微變化,我的這個簡單瞭解一下就好了,通常狀況下background也不會很消耗性能的。

popup是點擊browser_action或者page_action圖標時打開的一個小窗口網頁,焦點離開網頁就當即關閉,通常用來作一些臨時性的交互。

博客園網摘插件popup效果

popup能夠包含任意你想要的HTML內容,而且會自適應大小。能夠經過default_popup字段來指定popup頁面,也能夠調用setPopup()方法。

配置方式:

{
    "browser_action": { "default_icon": "img/icon.png", // 圖標懸停時的標題,可選 "default_title": "這是一個示例Chrome插件", "default_popup": "popup.html" } }

須要特別注意的是,因爲單擊圖標打開popup,焦點離開又當即關閉,因此popup頁面的生命週期通常很短,須要長時間運行的代碼千萬不要寫在popup裏面。

在權限上,它和background很是相似,它們之間最大的不一樣是生命週期的不一樣,popup中能夠直接經過chrome.extension.getBackgroundPage()獲取background的window對象。

4.6. injected-script

這裏的injected-script是我給它取的,指的是經過DOM操做的方式向頁面注入的一種JS。爲何要把這種JS單獨拿出來討論呢?又或者說爲何須要經過這種方式注入JS呢?

這是由於content-script有一個很大的「缺陷」,也就是沒法訪問頁面中的JS,雖然它能夠操做DOM,可是DOM卻不能調用它,也就是沒法在DOM中經過綁定事件的方式調用content-script中的代碼(包括直接寫onclickaddEventListener2種方式都不行),可是,「在頁面上添加一個按鈕並調用插件的擴展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中的代碼,後面我會在專門的一個消息通訊章節詳細介紹。

4.7. homepage_url

開發者或者插件主頁設置,通常會在以下2個地方顯示:

Chrome插件的8種展現形式

5.1. browserAction(瀏覽器右上角)

經過配置browser_action能夠在瀏覽器的右上角增長一個圖標,一個browser_action能夠擁有一個圖標,一個tooltip,一個badge和一個popup

示例配置以下:

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

5.1.1. 圖標

browser_action圖標推薦使用寬高都爲19像素的圖片,更大的圖標會被縮小,格式隨意,通常推薦png,能夠經過manifest中default_icon字段配置,也能夠調用setIcon()方法。

5.1.2. tooltip

修改browser_action的manifest中default_title字段,或者調用setTitle()方法。

5.1.3. badge

所謂badge就是在圖標上顯示一些文本,能夠用來更新一些小的擴展狀態提示信息。由於badge空間有限,因此只支持4個如下的字符(英文4個,中文2個)。badge沒法經過配置文件來指定,必須經過代碼實現,設置badge文字和顏色能夠分別使用setBadgeText()setBadgeBackgroundColor()

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

效果:

5.2. pageAction(地址欄右側)

所謂pageAction,指的是隻有當某些特定頁面打開才顯示的圖標,它和browserAction最大的區別是一個始終都顯示,一個只在特定狀況才顯示。

須要特別說明的是早些版本的Chrome是將pageAction放在地址欄的最右邊,左鍵單擊彈出popup,右鍵單擊則彈出相關默認的選項菜單:

而新版的Chrome更改了這一策略,pageAction和普通的browserAction同樣也是放在瀏覽器右上角,只不過沒有點亮時是灰色的,點亮了纔是彩色的,灰色時不管左鍵仍是右鍵單擊都是彈出選項:

具體是從哪一版本開始改的沒去仔細考究,反正知道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()] } ]); }); });

效果圖:

5.3. 右鍵菜單

經過開發Chrome插件能夠自定義瀏覽器的右鍵菜單,主要是經過chrome.contextMenusAPI實現,右鍵菜單能夠出如今不一樣的上下文,好比普通頁面、選中的文字、圖片、連接,等等,若是有同一個插件裏面定義了多個菜單,Chrome會自動組合放到以插件名字命名的二級菜單裏,以下:

5.3.1. 最簡單的右鍵菜單示例

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

效果:

5.3.2. 添加右鍵百度搜索

// 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)}); } });

效果以下:

5.3.3. 語法說明

這裏只是簡單列舉一些經常使用的,完整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);

5.4. override(覆蓋特定頁面)

使用override頁能夠將Chrome默認的一些特定頁面替換掉,改成使用擴展提供的頁面。

擴展能夠替代以下頁面:

  • 歷史記錄:從工具菜單上點擊歷史記錄時訪問的頁面,或者從地址欄直接輸入 chrome://history
  • 新標籤頁:當建立新標籤的時候訪問的頁面,或者從地址欄直接輸入 chrome://newtab
  • 書籤:瀏覽器的書籤,或者直接輸入 chrome://bookmarks

注意:

  • 一個擴展只能替代一個頁面;
  • 不能替代隱身窗口的新標籤頁;
  • 網頁必須設置title,不然用戶可能會看到網頁的URL,形成困擾;

下面的截圖是默認的新標籤頁和被擴展替換掉的新標籤頁。

代碼(注意,一個插件只能替代一個默認頁,如下僅爲演示):

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

5.5. devtools(開發者工具)

5.5.1. 預熱

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

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

  • 自定義一個和多個和ElementsConsoleSources等同級別的面板;
  • 自定義側邊欄(sidebar),目前只能自定義Elements面板的側邊欄;

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

自定義側邊欄(獲取當前頁面全部圖片):

5.5.2. devtools擴展介紹

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

來一張官方圖片:

每打開一個開發者工具窗口,都會建立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.extensionchrome.runtimeAPI,同時它也能夠像content-script同樣使用Message交互的方式與background頁面進行通訊。

5.5.3. 實例:建立一個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時的效果:

如下截圖示例的代碼:

// 檢測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)); }); });

5.5.4. 調試技巧

修改了devtools頁面的代碼時,須要先在 chrome://extensions 頁面按下Ctrl+R從新加載插件,而後關閉再打開開發者工具便可,無需刷新頁面(並且只刷新頁面不刷新開發者工具的話是不會生效的)。

因爲devtools自己就是開發者工具頁面,因此幾乎沒有方法能夠直接調試它,直接用 chrome-extension://extid/devtools.html"的方式打開頁面確定報錯,由於不支持相關特殊API,只能先本身寫一些方法屏蔽這些錯誤,調試通了再放開。

5.6. option(選項頁)

所謂options頁,就是插件的設置頁面,有2個入口,一個是右鍵圖標有一個「選項」菜單,還有一個在插件管理頁面:

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

咱們先看老版的options

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

這個頁面裏面的內容就隨你本身發揮了,配置以後在插件管理頁就會看到一個選項按鈕入口,點進去就是打開一個網頁,沒啥好講的。

效果:

再來看新版的optionsV2

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

options.html的代碼咱們沒有任何改動,只是配置文件改了,以後效果以下:

看起來是否是高大上了?

幾點注意:

  • 爲了兼容,建議2種都寫,若是都寫了,Chrome40之後會默認讀取新版的方式;
  • 新版options中不能使用alert;
  • 數據存儲建議用chrome.storage,由於會隨用戶自動同步;

5.7. omnibox

omnibox是向用戶提供搜索建議的一種方式。先來看個gif圖以便了解一下這東西究竟是個什麼鬼:

註冊某個關鍵字以觸發插件本身的搜索建議界面,而後能夠任意發揮了。

首先,配置文件以下:

{
    // 向地址欄註冊一個關鍵字以提供搜索建議,只能設置一個關鍵字 "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}); }) }

5.8. 桌面通知

Chrome提供了一個chrome.notificationsAPI以便插件推送桌面通知,暫未找到chrome.notifications和HTML5自帶的Notification的顯著區別及優點。

在後臺JS中,不管是使用chrome.notifications仍是Notification都不須要申請權限(HTML5方式須要申請權限),直接使用便可。

最簡單的通知:

代碼:

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

通知的樣式能夠很豐富:

這個沒有深刻研究,有須要的能夠去看官方文檔。

5種類型的JS對比

Chrome插件的JS主要能夠分爲這5類:injected scriptcontent-scriptpopup jsbackground jsdevtools js

6.1. 權限對比

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 能夠 能夠 不能夠

6.2. 調試方式對比

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

消息通訊

通訊主頁:https://developer.chrome.com/extensions/messaging

前面咱們介紹了Chrome插件中存在的5種JS,那麼它們之間如何互相通訊呢?下面先來系統概況一下,而後再分類細說。須要知道的是,popup和background其實幾乎能夠視爲一種東西,由於它們可訪問的API都同樣、通訊機制同樣、均可以跨域。

7.1. 互相通訊概覽

注:-表示不存在或者無心義,或者待驗證。

  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

7.2. 通訊詳細介紹

7.2.1. 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); }

7.2.2. 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

7.2.3. 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,一個先發送了,那麼另一個再發送就無效;

7.2.4. injected script和content-script

content-script和頁面內的腳本(injected-script天然也屬於頁面內的腳本)之間惟一共享的東西就是頁面的DOM元素,有2種方法能夠實現兩者通信:

  1. 能夠經過window.postMessagewindow.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); });

7.3. 長鏈接和短鏈接

其實上面已經涉及到了,這裏再單獨說明一下。Chrome插件中有2種通訊方式,一個是短鏈接(chrome.tabs.sendMessagechrome.runtime.sendMessage),一個是長鏈接(chrome.tabs.connectchrome.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: '我是你爸!'}); }); } });

其它補充

8.1. 動態注入或執行JS

雖然在backgroundpopup中沒法直接訪問頁面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'});

8.2. 動態注入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'});

8.3. 獲取當前窗口ID

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

8.4. 獲取當前標籤頁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); }); }); }

8.5. 本地存儲

本地存儲建議用chrome.storage而不是普通的localStorage,區別有好幾點,我的認爲最重要的2點區別是:

  • chrome.storage是針對插件全局的,即便你在background中保存的數據,在content-script也能獲取到;
  • chrome.storage.sync能夠跟隨當前登陸用戶自動同步,這臺電腦修改的設置會自動同步到其它電腦,很方便,若是沒有登陸或者未聯網則先保存到本地,等登陸了再同步至網絡;

須要聲明storage權限,有chrome.storage.syncchrome.storage.local2種方式可供選擇,使用示例以下:

// 讀取數據,第一個參數是指定要讀取的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('保存成功!'); });

8.6. 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"]);

8.7. 國際化

插件根目錄新建一個名爲_locales的文件夾,再在下面新建一些語言的文件夾,如enzh_CNzh_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.jsonCSS文件中經過__MSG_messagename__引入,如:

{
    "description": "__MSG_pluginDesc__", // 默認語言 "default_locale": "zh_CN", }

JS中則直接chrome.i18n.getMessage("helloWorld")

測試時,經過給chrome創建一個不一樣的快捷方式chrome.exe --lang=en來切換語言,如:

英文效果:

中文效果:

8.8. API總結

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

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

經驗總結

9.1. 查看已安裝插件路徑

已安裝的插件源碼路徑:C:\Users\用戶名\AppData\Local\Google\Chrome\User Data\Default\Extensions,每個插件被放在以插件ID爲名的文件夾裏面,想要學習某個插件的某個功能是如何實現的,看人家的源碼是最好的方法了:

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

9.2. 特別注意background的報錯

不少時候你發現你的代碼會莫名其妙的失效,找來找去又找不到緣由,這時打開background的控制檯才發現原來某個地方寫錯了致使代碼沒生效,正式因爲background報錯的隱蔽性(須要主動打開對應的控制檯才能看到錯誤),因此特別注意這點。

9.3. 如何讓popup頁面不關閉

在對popup頁面審查元素的時候popup會被強制打開沒法關閉,只有控制檯關閉了才能夠關閉popup,緣由很簡單:若是popup關閉了控制檯就沒用了。這種方法在某些狀況下很實用!

9.4. 不支持內聯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.

9.5. 注入CSS的時候必須當心

因爲經過content_scripts注入的CSS優先級很是高,幾乎僅次於瀏覽器默認樣式,稍不注意可能就會影響一些網站的展現效果,因此儘可能不要寫一些影響全局的樣式。

之因此強調這個,是由於這個帶來的問題很是隱蔽,不太容易找到,可能你正在寫某個網頁,昨天樣式仍是好好的,怎麼今天就忽然不行了?而後你辛辛苦苦找來找去,找了半天才發現居然是由於插件裏面的一個樣式影響的!

打包與發佈

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

而後會生成一個.crx文件,要發佈到Google應用商店的話須要先登陸你的Google帳號,而後花5個$註冊爲開發者,本人太窮,就懶得親自驗證了,有發佈需求的本身去整吧。

參考

11.1. 官方資料

推薦查看官方文檔,雖然是英文,可是全且新,國內的中文資料都比較舊(注意如下所有須要FQ):

11.2. 第三方資料

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

附圖

附圖:Chrome高清png格式logo:

相關文章
相關標籤/搜索