如何從零開始開發(fā)一個 Chrome 插件?
什么是瀏覽器插件?
簡單來說瀏覽器插件,是瀏覽器上的一種工具,可以提供一些瀏覽器沒有的功能,幫你做一些有趣的事情。開發(fā)者可以根據(jù)自己的喜歡,去實現(xiàn)一些功能。插件基于Web技術(shù)(html、css、js)構(gòu)建。
舉個栗子??
FeHelper.JSON插件

功能:格式化JSON、編碼轉(zhuǎn)化、markdown、代碼壓縮等功能。
二維碼生成器
功能:可以根據(jù)當(dāng)前瀏覽的網(wǎng)頁地址,生成一個二維碼。
SwitchyOmega Proxy

功能:你懂的。
Hello World
manifest.json
Chrome 瀏覽器插件沒有嚴(yán)格的文件結(jié)構(gòu)約束,只需要保證文件夾根目錄有 manifest.json 文件**,**該文件的內(nèi)容會概括插件所需的資源、權(quán)限等等。
一個段簡單的示例:
{
"manifest_version": 2, // 必填
"name": "my-plugin", // 必填
"version": "0.1.0" // 必填
}
manifest_version:代表了manifest文件的版本,瀏覽器會根據(jù)這個值去指定該版本擁有的功能。
name:插件的名稱。
version:插件版本。
將manifest.json文件放到一個文件夾內(nèi)。
chrome://extensions/
在瀏覽器地址欄輸入chrome://extensions/打開“拓展程序”頁面。
注意:需要啟用右上角的 “開發(fā)者模式” 才能加載已解壓的插件文件:

加載已解壓的插件
啟用之后點擊加載已解壓的拓展程序,選擇剛剛我們放入了manifest.json的文件夾,之后你會看到:

新增了一個我們剛剛添加的插件,而且瀏覽器右上角也會有我們的一個圖標(biāo):

如果沒有設(shè)置插件圖標(biāo),那么插件的第一個字符會成為插件的默認(rèn)icon。
讓插件看起來更“插件”一點
為了讓這個插件更“完善”一點,我們給它加一個icon和描述,并且點擊出現(xiàn)一個popup頁面,popup 頁面一般用來承載臨時性的交互,且生命周期很短:單擊圖標(biāo)打開popup,焦點離開又立即關(guān)閉,可以通過default_popup字段來定義。
{
.....
"description": "這是一段描述",
// 插件管理頁面的icon
"icons": {
"84": "./icon/ball.png"
},
// 瀏覽器右上角的圖標(biāo)和內(nèi)容
"browser_action": {
"default_icon": "./icon/ball.png",
"default_title": "我的插件",
"default_popup": "./html/popup.html"
}
}
此時我們的目錄結(jié)構(gòu)也變成了這樣:

給popup.html加上內(nèi)容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>my-plugin</title>
</head>
<body>
<p style="width: 200px;text-align:center;">hello world!!</p>
</body>
</html>
之后,我們點擊插件右下角的“刷新”按鈕:



manifest.json 配置介紹
background
{
...
"background": {
// 提供一個頁面給background
"page": "./html/background.html"
// 或者若干個js文件,后臺會默認(rèn)生成一個空白的html
"scripts": ["./js/background.js"]
}
}
background配置項,為插件的后臺常駐頁面,生命周期隨著瀏覽器的生命周期一樣,瀏覽器一啟動,后臺頁面就會開始運行,直到瀏覽器被關(guān)閉;或者在插件管理頁面,將該插件禁用了,后臺頁面也會停止運行。
另外,background擁有的權(quán)限比較高,幾乎可以調(diào)用所有的Chrome擴展API(除了devtools),同時擁有直接跨域的能力。
page:指定一個網(wǎng)頁為后臺頁面。
scripts:指定若干個js文件,后臺會自動生成一個html,并按順序調(diào)用這些js文件。
注意:page 和 scripts 選項只能二選一,不然會報錯。
配置好之后,屬性插件,會出現(xiàn)一個背景頁選項:

function _back() {
console.log('background.js')
}
console.log('running...')
點進去看看里面裝的什么玩意:

沒錯,是一個普通的后臺頁面,如果background.js和其他頁面有通信,則可以在這里進行查看請求或者調(diào)試代碼。
如果使用page選項,打開也是這個樣子。
另外:由于background是一直在后臺運行的,為了優(yōu)化性能,可以增加一個配置:
{
...
"background": {
...
"persistent": false
}
}
這樣,插件就會在被需要時加載,在空閑時被關(guān)閉。比如安裝、更新插件的時候,或者有其他頁面與background通信的時候才會被加載。
content-scripts
content-scripts能夠在合適的時機(頁面載入前、載入后、空閑時)注入腳本,允許內(nèi)容腳本更改其JavaScript環(huán)境,而不與頁面或其他內(nèi)容腳本發(fā)生沖突。
例如,原頁面有個按鈕,并且給按鈕添加了一個點擊事件:
<html>
<button id="mybutton">click me</button>
<script>
var greeting = "hello, ";
var button = document.getElementById("mybutton");
button.person_name = "Bob";
button.addEventListener("click", function() {
alert(greeting + button.person_name + ".");
}, false);
</script>
</html>
在content-scripts中,加入以下代碼:
var greeting = "hola, ";
var button = document.getElementById("mybutton");
button.person_name = "Roberto";
button.addEventListener("click", function() {
alert(greeting + button.person_name + ".");
}, false);
當(dāng)頁面運行之后,腳本內(nèi)容也會在插件定義的時間運行,當(dāng)頁面點擊按鈕時,會出現(xiàn)兩次彈窗。
content-scripts配置:
{
...
"content_scripts": [
{
// 在匹配的URL中運行,<all_urls>表示所有的URL都會運行。
"matches": ["<all_urls>"],
// 注入的js,會按順序運行。
"js": ["./js/content.js"],
// css引入需謹(jǐn)慎,因為可能會影響全局的樣式,同樣也能接收多個css文件,會按順序插入到頁面中
"css": ["./css/style.css"],
// 代碼注入的時機,可選值: "document_start", "document_end", or "document_idle",最后一個表示頁面空閑時,默認(rèn)document_idle
"run_at": "document_start"
},
{
"matches": ["https://www.baidu.com/"],
"js": ["./js/other.js"],
"run_at": "document_start"
}
],
...
}
content.js代碼如下:
console.log('hello, from content.js');
other.js代碼如下:
console.log('hello, from other.js...')
更新插件,當(dāng)在 https://bytedance.feishu.cn/drive/home/運行時:

因為【 https://bytedance.feishu.cn/drive/home/】只匹配到了<all_urls>的規(guī)則,所以之后運行content.js
當(dāng)在https://www.baidu.com/運行時:

content-scripts 和原始頁面共享DOM,但是不共享JS,如要訪問頁面JS(例如某個JS變量),只能通過inject-scripts來實現(xiàn)。content-scripts能夠訪問的Chrome API的權(quán)限也比較低,只能訪問以下四個API:
chrome.extension(getURL , inIncognitoContext , lastError , onRequest , sendRequest) chrome.i18n chrome.runtime(connect , getManifest , getURL , id , onConnect , onMessage , sendMessage) chrome.storage
Inject-scripts
inject-scripts 是通過DOM操作插入的JS代碼,通常在content-scripts只能操作DOM,但是卻無法訪問頁面的JS,借助content-scripts可以操作DOM的能力,往頁面中插入JS文件,給頁面提供調(diào)用插件API的能力,以及和background通信的能力。
在插入之前,需配置一下web可訪問的資源,同時content-scripts的調(diào)用時機換成"document_end"或者"document_idle",不然會無法獲取DOM,導(dǎo)致插入失敗。在manifest.json中添加以下內(nèi)容:
{
...
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["./js/content.js"],
"run_at": "document_end"
},
...
],
"web_accessible_resources": ["js/inject.js"],
...
}
inject.js的內(nèi)容如下:
function mockApi () {
console.log('this is from inject.js')
}
content.js增加以下代碼:
(function () {
let path = 'js/inject.js';
let script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
// 注意,路徑需用Chrome API 生成,這個方法可以獲得插件的資源的真實路徑。
// 類似:chrome-extension://ihcokhadfjfchaeagdoclpnjdiokfakg/js/inject.js
script.src = chrome.extension.getURL(path);
script.onload = function () {
// 在執(zhí)行完代碼之后移除script標(biāo)簽
this.parentNode.removeChild(this);
}
document.body.appendChild(script);
})();
更新插件后,頁面就可以訪問inject.js的方法:

permissions
插件后臺有的操作需要配置相應(yīng)的權(quán)限,例如本地存儲、網(wǎng)絡(luò)請求、通知等等,示例:
{
...
"permissions": [
"contextMenus", // 右鍵菜單
"tabs", // 標(biāo)簽
"notifications", // 通知
"webRequest", // web請求
"webRequestBlocking",
"storage" // 插件本地存儲
],
...
}
完整的manifest配置
官方文檔:https://developer.chrome.com/extensions/manifest
通信
popup和background通信
popup可以通過 chrome.extension.getBackgroundPage() API 直接獲取到background的上下文,從而調(diào)用background的方法來通信:
// popup.js
var backend = chrome.extension.getBackgroundPage();
backend.test(); // 訪問bbackground的函數(shù)
background可以通過chrome.extension.getViews({type:'popup'}) 獲取到popup的上下文,前提是popup頁面是打開的狀態(tài)下。
let views = chrome.extension.getViews({type:'popup'});
let popup = null
if(views.length > 0) {
popup = views[0];
// 直接訪問popup的函數(shù)
popup.test();
}
這里需要注意一點:
在popup頁面,你如果想編寫js,請將js編寫在一個文件里面,然后引入進來,不然會報錯,這是因為Chrome的安全政策規(guī)定的:https://developer.chrome.com/extensions/contentSecurityPolicy
popup錯誤示范:
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<p style="width: 200px;text-align:center;">hello world!!</p>
<script>
// 不能直接在里面寫
</script>
</body>
</html>
正確姿勢:
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<p style="width: 200px;text-align:center;">hello world!!</p>
<script src="../js/popup.js"></script>
</body>
</html>
content-scripts和background通信
content-scripts可以通過 chrome.runtime.sendMessage(message) 給background發(fā)送消息:
chrome.runtime.sendMessage('message content', (res) => {
console.log('from background:', res)
});
background通過chrome.runtime.onMessage.addListener()監(jiān)聽content-scripts發(fā)送的消息:
chrome.runtime.onMessage.addListener(function(message, sender, callback) {
console.log(mesasge); // meesage content
callback && callback('yes this from background')
});
background主動給content-scripts發(fā)消息,首先得查找要給哪個tab發(fā)消息,使用chrome.tabs.query 這個方法查找到tab,再使用chrome.tabs.sendMessage 方法給tab發(fā)消息:
// {active: true, currentWindow: true} 表示查找當(dāng)前屏幕下的active狀態(tài)的tab;
chrome.tabs.query({active: true, currentWindow: true}, function (tabs) {
chrome.tabs.sendMessage(tabs[0].id, 'message content', (res) => {
console.log('from content:', res)
});
});
content-scripts通過chrome.runtime.onMessage.addListener 去監(jiān)聽事件:
chrome.runtime.onMessage.addListener(function (message, sender, callback) {
console.log(message, sender)
callback && callback('yes this from content')
});
注意:
1.消息內(nèi)容可以直接發(fā)送JSON格式的對象。
2.popup和content的通信方式與上面一樣。
3.如果popup和background都監(jiān)聽了從content發(fā)來的消息,兩者都能收到監(jiān)聽消息,但是callback只會觸發(fā)一次,被誰觸發(fā)取決與誰先發(fā)送。
inject-scripts和content-scripts
inject-scripts和content-scripts通信有兩種方法:
1.window.postMessage發(fā)送,window.addEventListener接收
2.還有一種是自定義的DOM事件;
但是很少情況會是content-scripts去調(diào)inject-scripts,因為,可以,但是沒必要....content-scripts完全可以自己處理一些API的事件監(jiān)聽,況且inject-scripts也只是content-scripts生成并插入到DOM里面的,所以在content-scripts眼里,inject-scripts就是個弟弟...
但是,很多用戶觸發(fā)的事件,需要通過inject-scripts告訴content-scripts,content-scripts再給background通信并且去做一些事情,然后再發(fā)消息告訴inject-scripts,從這個角度看:content-scripts就是一個inject-scripts的工具人!
(扯平了,完美。)
inject-scripts給content-scripts發(fā)消息:
window.postMessage({"test": '你好!工具人!'}, '*');
content-scripts接收消息:
window.addEventListener("message", function(message) {
console.log('來了老弟!', message.data);
}, false);
同樣的,content-scripts給inject-scripts發(fā)消息是一樣的。
練練手:HTTP Header 插件
實現(xiàn)一個HTTP Header 插件,可以實現(xiàn)動態(tài)添加header,并且給網(wǎng)絡(luò)請求自動加上header,header參數(shù)可以配置。
示例圖:

Background 功能設(shè)計
background復(fù)制存儲、操作headers,對所有瀏覽器請求做一層攔截,并加上啟用的headers。
注意:因為涉及到網(wǎng)絡(luò)請求,所以需在manifest.json中添加權(quán)限:
{
...
"permissions": [
"storage", // 本地存儲
"webRequest", // 網(wǎng)絡(luò)請求
"webRequestBlocking", // 網(wǎng)絡(luò)請求 阻塞式
"<all_urls>" // 匹配的URL
]
...
}
Background 功能偽代碼:
// headers數(shù)據(jù)結(jié)構(gòu), 附帶默認(rèn)值;(可以改為本地存儲)。
const headers = [
{
key: 'Content-Type',
value: 'application/x-www-form-urlencoded',
enable: false,
},
{
key: 'Test-Header',
value: '按F進入坦克',
enable: true,
},
];
// 獲取、新增、刪除、啟用禁用
function getHeaders () {
return headers;
}
function addHeader (header) {
headers.push(header);
}
function deleteHeader (index) {
headers.splice(index, 1);
}
function toggleHeader(index) {
headers[index].enable = !headers[index].enable;
}
...
// 請求攔截器
// On install 在被安裝的時候去初始化
chrome.runtime.onInstalled.addListener(function(){
// 添加事件
chrome.webRequest.onBeforeSendHeaders.addListener(requestHandle, {
urls: ["<all_urls>"],// 攔截所有URL的請求
},["blocking", "requestHeaders"]); // 阻塞式
console.log('load');
});
// 添加header
function requestHandle(request) {
let requestHeaders = request.requestHeaders;
// 添加headers
headers.forEach(item => {
if (item.enable) {
requestHeaders.push({
name: item.key,
value: item.value,
});
}
});
return {requestHeaders};
}
chrome.webRequest的生命周期:

詳細參考:https://developer.chrome.com/extensions/webRequest
popup 頁面設(shè)計
popup頁面提供增加、刪除、啟用禁用功能接口,并且在每次打開popup頁面的時候去background獲取最新的header數(shù)據(jù),展示在前臺。
popup.js 功能偽代碼:
// popup頁面被打開時,去后臺獲取最新header
window.onload = function () {
let backend = chrome.extension.getBackgroundPage();
// 調(diào)用background方法,獲得headers
let headers = backend.getHeaders();
// 渲染header
createElement(headers);
}
// 增加按鈕
function addHeader() {
let backend = chrome.extension.getBackgroundPage();
let key = document.querySelector('.key');
let value = document.querySelector('.value');
let header = {
key: key.value,
value: value.value,
enable: true
}
// 調(diào)用background方法,新增headers
backend.addHeader(header);
createElement(header);
}
// 啟用禁用、刪除功能
function toggleHeader(index) {
let backend = chrome.extension.getBackgroundPage();
backend.toggleHeader(index);
}
function delHeader(index) {
let backend = chrome.extension.getBackgroundPage();
backend.deleteHeader(index);
}
效果
打開popup,添加一個header:


總結(jié)
很多權(quán)限、功能需要在manifest.json配置。 content-scripts、popup、background、inject-scripts擁有的權(quán)限不一樣,通信方式也不一樣,理解各個腳本的特點,組合使用。 開發(fā)調(diào)試可在后臺背景頁查看信息,popup、inject-scripts、content-scripts可直接審查元素調(diào)試。
Chrome 插件還有很多功能這里沒有詳細介紹,例如devtools。感興趣的同學(xué)可以查閱下面的參考文檔。
參考文檔
官方文檔:https://developer.chrome.com/extensions

簡歷投遞聯(lián)系郵箱「 [email protected] 」
點擊閱讀原文,快來加入我們吧!
