入門chrome插件開發教程和經驗總結,一篇就搞掂!


版權聲明:本文為博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
本文鏈接: https://blog.csdn.net/weixin_44244857/article/details/85232668

前言

關於chrome extension的開發經驗總結或說明文檔等資料很多,很多人在寫,然而,我也是一員。但是,也許這篇文章,可能給你一些不一樣的感受。
這里介紹的是80%你要開發擴展會碰到的問題

前面部分大多數是一些基礎介紹,和別人的資料大同小異,但是用的是通俗的語言或者我自己理解來描述的,不是拷貝官方的描述,不然的話,你干脆看官方文檔就好啦,干嘛還來我這里折騰對吧,也許這些通俗的描述,更方便你理解(當然不排除也會有官方的話語)
后面部分多為一些我在項目中總結的方法,這部分就是在別人的資料可能看不到的地方了,當然,這些方法也許不通用,因為畢竟是基於我項目里的,但是盡量總結一套方法出來。

廢話不多說,咱們開始吧…


目錄

WHAT

谷歌擴展(chrome extension),在認識之前,首先要明確一個觀念,這種擴展程序,實際上不是一個exe、app之類的程序,下載了本地打開運行安裝,本質上,它就是一個網頁,寫的用的都是前端的語言,高檔點說是一個程序,通俗來講, 就是運行在瀏覽器上的一個網站,網頁。

我這種說法也許不對,不准確,不專業。但是起碼,能把小白開發擴展的心態,調整好點,實際上是一個不難的東西,就是在寫頁面而已。要知道,心態不好,后面就堅持不下去了。

最基本組成

這里講的是開發一個擴展(插件)最常用最基本的所需的東西,並不像官方說的那種分類。

  • manifest.json
  • background script
  • content script
  • popup

嚴格上來講主要是background script 、 content script 和 popup,畢竟他們都是貫穿在manifest里的,把manifest寫出來,只是為了凸顯一下它的重要性

(一)manifest.json

一個插件,必須都含有這個一個文件——manifest.json,位於根目錄。顧名思義,這是一個擴展的組成清單,在這個清單里能大約看到該插件的一個“規則”。

羅列和簡單介紹一下一些常用的配置項,說之前,先看一個大致的文件,首先感官感受一下先

{ // 必須 "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/**/*" ] }

上面有我寫的一些注釋,用於幫助大家更好的去理解。
那接下來開始說一下其中的配置項

icons

extension程序的圖標,可以有一個或多個。
48x48的圖標用在extensions的管理界面(chrome://extensions);
128x128 的圖標用在安裝extension程序的時候;
16x16 的圖標當作 extension 的頁面圖標,也可以顯示在信息欄上。
圖標一般為PNG格式, 因為最好的透明度的支持,不過WebKit支持任何格式,包括BMP,GIF,ICO等
注意: 以上寫的圖標不是固定的。隨瀏覽器的環境的改變而變。如:安裝時彈出的對話框變小。

browser_action與page_action

這兩個配置項都是用來處理擴展在瀏覽器工具欄上的表現行為。
前者擴展可以適用於任何頁面。后者擴展只能作用於某一頁面,當打開該頁面時觸發該Google Chrome擴展,關閉頁面則Google Chrome擴展也隨之消失。

通俗的舉個例子,一些擴展任何頁面可用,就都會顯示在工具欄上為可用狀態,一些擴展只適用於某些頁面,如大家很熟悉的vue tools調試器,在檢測到頁面用的是vue時,就會在工具欄顯示出來並可用(非灰色)

default_popup

在用戶點擊擴展程序圖標時,都可以設置彈出一個popup頁面。而這個頁面中自然是可以有運行的js腳本的(比如就叫popup.js)。它會在每次點擊插件圖標——popup頁面彈出時,重新載入。

這個小小的設置,也就是上面我把它分為在基本組成里的popup了

permissions

在background里使用一些chrome api,需要授權才能使用,例如要使用chrome.tabs.xxx的api,就要在permissions引入“tabs”

web_accessible_resources

允許擴展外的頁面訪問的擴展內指定的資源。通俗來講就是,擴展是一個文件夾A的,別人的網站是一個文件夾B,B要看A的東西,需要獲得權限,而寫在這個屬性下的文件,就是授予了別人訪問的權限。

(二)background script

background可以理解為插件運行在瀏覽器中的一個后台網站/腳本,注意它是與當前瀏覽頁面無關的。
實際上這部分內容的配置情況也會寫在manifest里,對應的是background配置項。單獨拿出來講,是彰顯它的分量很重,也是一個插件常用的配置。從其中幾個配置項項去了解一下什么是background script

page

可以理解為這個后台網站的主頁,在這個主頁中,有引用的腳本,其中一般都會有一個專門來管理插件各種交互以及監聽瀏覽器行為的腳本,一般都起名為background.js。這個主頁,不一定要求有。

scripts

這里的腳本其實跟寫在page里html引入的腳本目的一樣,個人的理解是,page的html在沒有的情況下,那么腳本就需要通過這個屬性引入了;
如果在存在page的情況下,一般在這里引入的腳本是專門為插件服務的腳本,而那些第三方腳本如jquery還是在page里引用比較好,或許這是一個眾人的“潛規則”吧

persistent

所謂的后台腳本,在chrome擴展中又分為兩類,分別運行於后台頁面(background page)和事件頁面(event page)中。兩者區別在於,

前者(后台頁面)持續運行,生存周期和瀏覽器相同,即從打開瀏覽器到關閉瀏覽器期間,后台腳本一直在運行,一直占據着內存等系統資源,persistent設為true;

而后者(事件頁面)只在需要活動時活動,在完全不活動的狀態持續幾秒后,chrome將會終止其運行,從而釋放其占據的系統資源,而在再次有事件需要后台腳本來處理時,重新載入它,persistent設為false。

保持后台腳本持久活動的唯一場合是擴展使用chrome.webRequest API來阻止或修改網絡請求。webRequest API與非持久性后台頁面不兼容。

(三) content script

這部分腳本,簡單來說是插入到網頁中的腳本。它具有獨立而富有包容性。

所謂獨立,指它的工作空間,命名空間,域等是獨立的,不會說跟插入到的頁面的某些函數和變量發生沖突;

所謂包容性,指插件把自己的一些腳本(content script)插入到符合條件的頁面里,作為頁面的腳本,因此與插入的頁面共享dom的,即用dom操作是針對插入的網頁的,在這些腳本里使用的window對象跟插入頁面的window是一樣的。主要用在消息傳遞上(使用postMessage和onmessage)

實際上這部分內容的配置情況也會寫在manifest里,對應的是content_scripts配置項。單獨拿出來講,是彰顯它的分量很重,也是一個插件常用的配置。從其中幾個配置項項去了解一下什么是content script

js

要插入到頁面里的腳本。例子很常見,例如在一個別人的網頁上,你要打開你做的擴展,對別人的網頁做一些處理或者獲取一些數據等,那怎么跟別人的頁面建立起聯系呢?就是通過把js里的這些腳本嵌入都別人的網頁里。

matches

必需。匹配規則組成的數組,用來匹配頁面url的,符合條件的頁面將會插入js的腳本。當然,有可以匹配的自然會有不匹配的——exclude_matches。匹配規則:

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

上面的官方描述已經很清晰啦,我就不多說了。

run_at

js配置項里的腳本何時插入到頁面里呢,這個配置項來控制插入時機。有三個選擇項:

  • document_start
  • document_end
  • document_idle(默認)
document_start

style樣式加載好,dom渲染完成和腳本執行前

document_end

dom渲染完成后,即DOMContentLoaded后馬上執行

document_idle

在DOMContentLoaded 和 window load之間,具體是什么時刻,要視頁面的復雜程度和加載時間,並針對頁面加載速度進行了優化。

popup

其實這部分,早就講過了,就是在manifest里的browser_actionpage_action配置項里設置的


基礎的通信機制

上面講述了基本的組成部分,那么這幾部分,他們要進行交流合作,把他們組織起來,才能成就一個漂亮的擴展。那么這種交流,分為以下幾種說明:

  • content script與background的通信
  • popup與background的通信
  • popup與content script的通信
  • 插件iframe網站與插入網頁的通信

最后一點,是額外說的,但是卻是很重要的。畢竟很多擴展,也是以iframe的形式呈現的。

(一)content script與background的通信

content-script向background發送消息

在content-script端

使用

chrome.runtime.sendMessege( message, function(response) {…} )

 

 

就能向background發送消息了,第一個參數message為發送的消息(基礎數據類型),回調函數里的第一個參數為background接收消息后返回的消息(如有)

在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有多個(一個頁面一個),那么background怎么向特定的content-script發送消息?

在background端

首先我們需要知道要向哪個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接收到消息后的回傳消息

在content scripts端

同樣是使用

chrome.runtime.onMessege.addListener(function(request, sender, sendResponse) {…})

進行來自background發來消息的監聽並回傳

(二)popup與background的通信

一般地,popup與background的交流,常見於popup要獲取background里的某些“東西”,當然我們可以使用上述的chrome.runtime.sendMessagechrome.runtime.onMessage的方式進行popup向background的交流,但是其實有更方便快捷的方式:

var bg = chrome.extension.getBackgroundPage(); bg.someMethod(); //someMethod()是background中的一個方法

 

(三)popup與content script的通信

這里的通信,實際上跟background與content script的方式是一樣的

(四)插件iframe網站與插入網頁的通信

其實這兩個的通信,算不上是chrome extension開發里的知識,它就是一個基礎的js知識——ifame與父窗體的通信。

同域的情況下,可以通過DOM操作達到通信的目的,如獲取dom元素,獲取值賦值之類的。
在父窗體里,用window.contentWindow獲取到iframe的window對象
在iframe里,用window.parent獲取到父窗體的window對象

而在跨域下,上述的方法是行不通的,網上也有各種方法解決,但是在插件這塊里,最方便的就是使用js的message機制了。
我這里說的message機制,就是使用window對象的postMessage()onmessage

一般插件展現都是在別人的網站上,因此沒辦法直接在別人的網站上添加postMessageonmessage的代碼。這時候,重任就落在了插件的content script身上了(之前說了他們共用DOM)。由於content script是自己編寫的,所以可以“為所欲為”了

iframe向父窗體發送消息

在iframe端

假設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,然后在這邊判斷一下等。當然,這樣也不能百分百確定,只能說保障更上一層樓了。
如果大家有好的點子,請務必告訴鄙人!受教受教!

父窗體向iframe發送消息

在父窗體端

使用 extensionIframe.contentWindow.postMessage(message, '*');
其中extensionIframe為插件的iframe節點對象,message為發送的消息,例如

{from: 'content-script', other: xxx}

在iframe端

使用

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請求,我的一套“土辦法”

我們知道,在進行ajax請求,是有可能遇到跨域的。例如我的項目就是在任何一個頁面插入iframe網站,然后有些操作就需要發請求了,這樣必然存在跨域問題。
然而,如果開發插件還要開發者想辦法解決跨域問題,那chrome extension就太遜了,而且,跨不跨域,還不是瀏覽器自己的主意,是瀏覽器本身的安全策略。

所以,chrome extension為了保證自己的優越性,允許在自己的程序里面,實現跨域請求,那完全的chrome extension程序,無非就是在background里了。

因此,插件要實現一些ajax請求,都得通通搬到background里實現。這個事情,本身不是什么重大發現。接下來要說的是,我利用這個特性,按照某個規則,實現一套方便的請求流程。

這里以一個ifame網站嵌入到別人頁面的這類形式的chrome extension為例子。

在插件生成的iframe網站里

首先在這個插件網站中,有一些按鈕操作本身是要觸發某些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

在插件的content script里

監聽發來的消息,這里①標注的代碼為前面說過的區分來源,這里重點放在②部分的代碼

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

在插件的background script里

監聽剛轉發過來的消息

// 這是所有請求組成對象 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; });

再返回到插件生成的iframe網站里

這次繞回到這個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,可能不單單是通過點擊瀏覽器工具欄上的插件圖標來激活插件,也有一些需求是通過點擊網站上某個按鈕來激活插件(如自家的系統),那么這時候第一步需要的是,檢測瀏覽器是否安裝了要求的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的基本介紹和開發思路就介紹到這里,后續會有一篇文章專門來闡述一下,我項目中遇到的需求,遇到的問題以及對應的解決方案。

感興趣的可以關注一下,感覺文章寫得對你有幫助的話,請點下贊。

轉載請標注出處謝謝,寫文章不易。

首發地----> 戳me


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM