如何從零開始開發一個 Chrome 插件?

2021-02-24 字節前端 ByteFE
什麼是瀏覽器插件?

簡單來說瀏覽器插件,是瀏覽器上的一種工具,可以提供一些瀏覽器沒有的功能,幫你做一些有趣的事情。開發者可以根據自己的喜歡,去實現一些功能。插件基於Web技術(html、css、js)構建。

舉個慄子🌰FeHelper.JSON插件

功能:格式化JSON、編碼轉化、markdown、代碼壓縮等功能。

二維碼生成器

功能:可以根據當前瀏覽的網頁地址,生成一個二維碼。

SwitchyOmega Proxy

功能:你懂的。

Hello Worldmanifest.json

Chrome 瀏覽器插件沒有嚴格的文件結構約束,只需要保證文件夾根目錄有 manifest.json 文件**,**該文件的內容會概括插件所需的資源、權限等等。

一個段簡單的示例:

{
    "manifest_version": 2, // 必填
    "name": "my-plugin", // 必填
    "version": "0.1.0" // 必填
}

manifest_version:代表了manifest文件的版本,瀏覽器會根據這個值去指定該版本擁有的功能。

name:插件的名稱。

version:插件版本。

將manifest.json文件放到一個文件夾內。

chrome://extensions/

在瀏覽器地址欄輸入chrome://extensions/打開「拓展程序」頁面。

注意:需要啟用右上角的 「開發者模式」 才能加載已解壓的插件文件:


加載已解壓的插件

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

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

此時已經加載了一個插件了,但是這個插件除了佔用瀏覽器的一個位置除外,沒有任何作用。

如果沒有設置插件圖標,那麼插件的第一個字符會成為插件的默認icon。

讓插件看起來更「插件」一點

為了讓這個插件更「完善」一點,我們給它加一個icon和描述,並且點擊出現一個popup頁面,popup 頁面一般用來承載臨時性的交互,且生命周期很短:單擊圖標打開popup,焦點離開又立即關閉,可以通過default_popup欄位來定義。

{
  

  "description": "這是一段描述",
  // 插件管理頁面的icon
  "icons": {
    "84": "./icon/ball.png"
  },
  // 瀏覽器右上角的圖標和內容
  "browser_action": {
    "default_icon": "./icon/ball.png",
    "default_title": "我的插件",
    "default_popup": "./html/popup.html"
  }
}

此時我們的目錄結構也變成了這樣:

給popup.html加上內容:

<!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>

之後,我們點擊插件右下角的「刷新」按鈕:

並且右上角的icon也變了,點擊一下,會彈出我們剛剛編寫的popup.html頁面:

manifest.json 配置介紹background
{
    ...
    "background": {
        // 提供一個頁面給background
        "page": "./html/background.html"
        // 或者若干個js文件,後臺會默認生成一個空白的html
        "scripts": ["./js/background.js"]
    }
}

background配置項,為插件的後臺常駐頁面,生命周期隨著瀏覽器的生命周期一樣,瀏覽器一啟動,後臺頁面就會開始運行,直到瀏覽器被關閉;或者在插件管理頁面,將該插件禁用了,後臺頁面也會停止運行。

另外,background擁有的權限比較高,幾乎可以調用所有的Chrome擴展API(除了devtools),同時擁有直接跨域的能力。

page:指定一個網頁為後臺頁面。

scripts:指定若干個js文件,後臺會自動生成一個html,並按順序調用這些js文件。

注意:pagescripts 選項只能二選一,不然會報錯。

配置好之後,屬性插件,會出現一個背景頁選項:

function _back() {
  console.log('background.js')
}
console.log('running...')

點進去看看裡面裝的什麼玩意:

沒錯,是一個普通的後臺頁面,如果background.js和其他頁面有通信,則可以在這裡進行查看請求或者調試代碼。

如果使用page選項,打開也是這個樣子。

另外:由於background是一直在後臺運行的,為了優化性能,可以增加一個配置:

{
    ...
    "background": {
        ...
        "persistent": false
    }
}

這樣,插件就會在被需要時加載,在空閒時被關閉。比如安裝、更新插件的時候,或者有其他頁面與background通信的時候才會被加載。

content-scripts

content-scripts能夠在合適的時機(頁面載入前、載入後、空閒時)注入腳本,允許內容腳本更改其JavaScript環境,而不與頁面或其他內容腳本發生衝突。

例如,原頁面有個按鈕,並且給按鈕添加了一個點擊事件:

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

當頁面運行之後,腳本內容也會在插件定義的時間運行,當頁面點擊按鈕時,會出現兩次彈窗。

content-scripts配置:

{
    ...
    "content_scripts": [
        {
          // 在匹配的URL中運行,<all_urls>表示所有的URL都會運行。
          "matches": ["<all_urls>"],
          // 注入的js,會按順序運行。
          "js": ["./js/content.js"],
          // css引入需謹慎,因為可能會影響全局的樣式,同樣也能接收多個css文件,會按順序插入到頁面中
          "css": ["./css/style.css"],
          // 代碼注入的時機,可選值: "document_start", "document_end", or "document_idle",最後一個表示頁面空閒時,默認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...')

更新插件,當在 https://bytedance.feishu.cn/drive/home/運行時:

因為【 https://bytedance.feishu.cn/drive/home/】只匹配到了<all_urls>的規則,所以之後運行content.js

當在https://www.baidu.com/運行時:

同時命中了2個規則,所以content.js和other.js都會運行,順序也是正確的。

content-scripts 和原始頁面共享DOM,但是不共享JS,如要訪問頁面JS(例如某個JS變量),只能通過inject-scripts來實現。content-scripts能夠訪問的Chrome API的權限也比較低,只能訪問以下四個API:

chrome.extension(getURL , inIncognitoContext , lastError , onRequest , sendRequest)chrome.runtime(connect , getManifest , getURL , id , onConnect , onMessage , sendMessage)Inject-scripts

inject-scripts 是通過DOM操作插入的JS代碼,通常在content-scripts只能操作DOM,但是卻無法訪問頁面的JS,藉助content-scripts可以操作DOM的能力,往頁面中插入JS文件,給頁面提供調用插件API的能力,以及和background通信的能力。

在插入之前,需配置一下web可訪問的資源,同時content-scripts的調用時機換成"document_end"或者"document_idle",不然會無法獲取DOM,導致插入失敗。在manifest.json中添加以下內容:

{
    ...
    "content_scripts": [
        {
          "matches": ["<all_urls>"],
          "js": ["./js/content.js"],
          "run_at": "document_end"
        },
        ...
     ],
    "web_accessible_resources": ["js/inject.js"],
    ...
}

inject.js的內容如下:

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 () {
    // 在執行完代碼之後移除script標籤
    this.parentNode.removeChild(this);
  }
  document.body.appendChild(script);
})();

更新插件後,頁面就可以訪問inject.js的方法:

permissions

插件後臺有的操作需要配置相應的權限,例如本地存儲、網絡請求、通知等等,示例:

{
    ...
    "permissions": [
        "contextMenus", // 右鍵菜單
        "tabs", // 標籤
        "notifications", // 通知
        "webRequest", // web請求
        "webRequestBlocking",
        "storage" // 插件本地存儲
    ],
    ...
}

完整的manifest配置

官方文檔:https://developer.chrome.com/extensions/manifest

通信popup和background通信

popup可以通過 chrome.extension.getBackgroundPage() API 直接獲取到background的上下文,從而調用background的方法來通信:

// popup.js
var backend = chrome.extension.getBackgroundPage();
backend.test(); // 訪問bbackground的函數

background可以通過chrome.extension.getViews({type:'popup'}) 獲取到popup的上下文,前提是popup頁面是打開的狀態下。

let views = chrome.extension.getViews({type:'popup'});
let popup = null
if(views.length > 0) {
    popup = views[0];
    // 直接訪問popup的函數
    popup.test();
}

這裡需要注意一點:

在popup頁面,你如果想編寫js,請將js編寫在一個文件裡面,然後引入進來,不然會報錯,這是因為Chrome的安全政策規定的: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發送消息:

chrome.runtime.sendMessage('message content', (res) => {
    console.log('from background:', res)
});

background通過chrome.runtime.onMessage.addListener()監聽content-scripts發送的消息:

chrome.runtime.onMessage.addListener(function(message, sender, callback) {
   console.log(mesasge); // meesage content
   callback && callback('yes this from background')
});

background主動給content-scripts發消息,首先得查找要給哪個tab發消息,使用chrome.tabs.query 這個方法查找到tab,再使用chrome.tabs.sendMessage 方法給tab發消息:

// {active: true, currentWindow: true} 表示查找當前屏幕下的active狀態的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 去監聽事件:

chrome.runtime.onMessage.addListener(function (message, sender, callback) {
    console.log(message, sender)
    callback && callback('yes this from content')
});

注意:

1.消息內容可以直接發送JSON格式的對象。

2.popup和content的通信方式與上面一樣。

3.如果popup和background都監聽了從content發來的消息,兩者都能收到監聽消息,但是callback只會觸發一次,被誰觸發取決與誰先發送。

inject-scripts和content-scripts

inject-scripts和content-scripts通信有兩種方法:

1.window.postMessage發送,window.addEventListener接收

2.還有一種是自定義的DOM事件;

但是很少情況會是content-scripts去調inject-scripts,因為,可以,但是沒必要....content-scripts完全可以自己處理一些API的事件監聽,況且inject-scripts也只是content-scripts生成並插入到DOM裡面的,所以在content-scripts眼裡,inject-scripts就是個弟弟...

但是,很多用戶觸發的事件,需要通過inject-scripts告訴content-scripts,content-scripts再給background通信並且去做一些事情,然後再發消息告訴inject-scripts,從這個角度看:content-scripts就是一個inject-scripts的工具人!

(扯平了,完美。)

inject-scripts給content-scripts發消息:

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

content-scripts接收消息:

window.addEventListener("message", function(message) {
    console.log('來了老弟!', message.data);
}, false);

同樣的,content-scripts給inject-scripts發消息是一樣的。

練練手:HTTP Header 插件

實現一個HTTP Header 插件,可以實現動態添加header,並且給網絡請求自動加上header,header參數可以配置。

示例圖:

Background 功能設計

background複製存儲、操作headers,對所有瀏覽器請求做一層攔截,並加上啟用的headers。

注意:因為涉及到網絡請求,所以需在manifest.json中添加權限:

{
    ...
    "permissions": [
        "storage", // 本地存儲
        "webRequest", // 網絡請求
        "webRequestBlocking", // 網絡請求 阻塞式
        "<all_urls>" // 匹配的URL
    ]
    ...
}

Background 功能偽代碼:

// headers數據結構, 附帶默認值;(可以改為本地存儲)。
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 頁面設計

popup頁面提供增加、刪除、啟用禁用功能接口,並且在每次打開popup頁面的時候去background獲取最新的header數據,展示在前臺。

popup.js 功能偽代碼:

// popup頁面被打開時,去後臺獲取最新header
window.onload = function () {
    let backend = chrome.extension.getBackgroundPage();
    // 調用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
      }
    // 調用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:

隨便打開一個網頁,打開控制臺查看RequestHeaders:

總結
很多權限、功能需要在manifest.json配置。content-scripts、popup、background、inject-scripts擁有的權限不一樣,通信方式也不一樣,理解各個腳本的特點,組合使用。開發調試可在後臺背景頁查看信息,popup、inject-scripts、content-scripts可直接審查元素調試。

Chrome 插件還有很多功能這裡沒有詳細介紹,例如devtools。感興趣的同學可以查閱下面的參考文檔。

參考文檔

官方文檔:https://developer.chrome.com/extensions

參考博客:https://www.cnblogs.com/liuxianan/p/chrome-plugin-develop.html

相關焦點

  • 效率工程篇-如何開發一個Chrome插件(二)
    前言書從上文:《效率工程篇-如何開發一個Chrome插件(一)》昨天我們簡要學習開發了一個簡單的Chrome插件,了解了開發Chrome插件的基本原理,但是相對於大多數的前端Coder來說,用習慣了各種前端開發框架之後,基於HTML,JavaScript,Css的原生開發多多少少會有些許的不方便
  • chrome插件入門及如何利用react進行開發
    chrome 插件已經成為了我們工作中必不可少的工具,但是大部分同學都沒有去真正了解過 chrome 插件是如何開發的。本文就是帶大家一起了解學習如何開發 chrome 插件以及如何利用 react 進行 chrome 插件的開發。什麼是 chrome 插件?從 Google 的介紹中直譯過來,我們通常說的「chrome 插件」其實是指「chrome 擴展」,不過由於大家都已經習慣於叫它作為插件,所以我們也繼續以「插件」的名稱來稱呼。
  • 開發一個 Chrome 瀏覽器插件,攏共分幾步?
    但事實上,需求就意味著機會,你完全可以自己開發一個,不但可以讓自己方便,還能靠出售插件帶來不小的收益~Chrome 插件的開發其實並不困難,開發語言都是前端同學非常熟悉的:HTML、CSS 和 JavaScript,再加上 Chrome 提供了大量功能強大的接口進行調用,我們能夠非常簡單迅速地實現一個滿足自己需求的插件。
  • Chrome插件開發教程
    Chrome插件開發知識要點!! 本文先介紹插件的幾個核心知識點,再從項目實戰的角度深刻記憶它。簡介谷歌瀏覽器右上角的擴展插件(Chrome Extension)實際上是是更底層的瀏覽器功能擴展。Chrome插件是一個用Web技術開發、用來增強瀏覽器功能的軟體,它其實就是一個由HTML、CSS、JS、圖片等資源組成的一個.crx後綴的壓縮包。核心介紹manifest.json這是一個Chrome插件最重要也是必不可少的文件,用來配置所有和插件相關的配置,必須放在根目錄。
  • Chrome插件開發指南
    之前介紹過一個基於Chrome插件開發的無埋點可視化工具那麼,這篇文章就系統介紹一下Chrome插件開發,代碼連結附文後。
  • 【Chrome】678- Chrome插件開發全攻略
    插件是更底層的瀏覽器功能擴展,可能需要對瀏覽器源碼有一定掌握才有能力去開發。鑑於Chrome插件的叫法已經習慣,本文也全部採用這種叫法,但讀者需深知本文所描述的Chrome插件實際上指的是Chrome擴展。Chrome插件是一個用Web技術開發、用來增強瀏覽器功能的軟體,它其實就是一個由HTML、CSS、JS、圖片等資源組成的一個.crx後綴的壓縮包.
  • 使用 React.js 開發 Chrome 插件
    這裡非要類比的話,我理解chrome.* API就像我們開發Hybird應用一樣,需要有一個bridge層來提供底層原生的能力給js。我是做Android開發出生的,這只是我的個人理解,可能對大Web技術的理解還是不夠。其實Chrome上的插件,從UI上主要分成兩類:一類是瀏覽器按鈕(BrowserAction),另一類是頁面按鈕(PageAction)。
  • Chrome 插件開發入門
    什麼是Chrome插件        Chrome插件是一個用Web技術開發、用來增強瀏覽器功能的軟體,它其實就是一個由
  • 從 0 開始入門 Chrome Ext 安全(一) -- 了解一個 Chrome Ext
    並在19年4月8日,Edge正式放出了基於Chromium開發的Edge Dev瀏覽器,並提供了兼容Chrome Ext的配套插件管理。再加上國內的大小國產瀏覽器大多都是基於Chromium開發的,Chrome的插件體系越來越影響著廣大的人群。
  • Chrome插件(擴展)開發全攻略
    真正意義上的Chrome插件是更底層的瀏覽器功能擴展,可能需要對瀏覽器源碼有一定掌握才有能力去開發。鑑於Chrome插件的叫法已經習慣,本文也全部採用這種叫法,但讀者需深知本文所描述的Chrome插件實際上指的是Chrome擴展。Chrome插件是一個用Web技術開發、用來增強瀏覽器功能的軟體,它其實就是一個由HTML、CSS、JS、圖片等資源組成的一個.crx後綴的壓縮包.
  • Chrome擴展插件開發小感想
    ,開發的過程中調試極其不方便,所以想寫一個chrome插件來輔助使用react-model這篇文章的主要內容分為以下4個點開發谷歌瀏覽器插件的工程目錄以及其文件作用幾種js的對比以及通信調試技巧注意事項(我踩過的坑...)
  • 【乾貨】Chrome插件(擴展)開發全攻略
    插件是更底層的瀏覽器功能擴展,可能需要對瀏覽器源碼有一定掌握才有能力去開發。鑑於Chrome插件的叫法已經習慣,本文也全部採用這種叫法,但讀者需深知本文所描述的Chrome插件實際上指的是Chrome擴展。Chrome插件是一個用Web技術開發、用來增強瀏覽器功能的軟體,它其實就是一個由HTML、CSS、JS、圖片等資源組成的一個.crx後綴的壓縮包.
  • 手把手帶你學會 Chrome 插件開發
    但是我們並不總是能夠找到符合我們所需要功能的插件,只能退而求其次地選擇其它工具。而事實上,你完全可以選擇自己開發一個。Chrome 插件的開發其實並不困難,開發語言都是前端同學非常熟悉的:HTML、CSS 和 JavaScript,再加上 Chrome 提供了大量功能強大的接口進行調用,我們能夠非常簡單迅速地實現一個滿足自己需求的插件。
  • Chrome開發工具插件推薦
    想要了解一個網站的技術棧時就用它。Wappalyzer 可以分析網站所用的各項技術。它甚至可以幫助我們隨時了解市場上的新興技術信息。我很喜歡這個插件,你可以查看所選部分或元素的完整 CSS,也可以在線編輯它的 CSS。推薦設計師和非設計師使用。
  • Chrome擴展入門開發瀏覽器插件
    這次的練習是做一個Chrome的擴展,分享一下入門開發過程。。
  • Chrome插件如何打包
    ,也有眾多的開發者擁簇但是在國內的環境下,由於科學上網的原因,眾多使用者無法去訪問 Chrome 網上應用商店,這對於插件的使用造成了極大的阻礙1、場景為方便使用者獲取插件,需要將開發好的插件打包成 crx 格式,上傳網盤,供用戶下載使用2、思考
  • Yandex 一個可以安裝 chrome插件的手機瀏覽器
    瀏覽器插件你沒看錯,它真的能安裝chrome的插件不過呢,雖然功能強大但是安裝插件還是需要有點技術的再開始之前先給大家普及點瀏覽器小知識插件管理界面:chrome://extensions/chrome插件商城:https://chrome.google.com/webstore/category/extensionsBrowser瀏覽器支持直接在chrom插件商城安裝插件
  • 利用Vue 腳手架 開發chrome 插件,太方便了!
    前言一個 Chrome 插件,
  • 我的Chrome瀏覽器插件
    Chrome改變了我的瀏覽方式在我日常開發以及娛樂生活中,瀏覽器是我使用頻率較高的一個應用,當我大學擁有第一部電腦開始,之後不論電腦換成什麼
  • 推薦 10 款 Chrome 開發工具插件
    想要了解一個網站的技術棧時就用它。Wappalyzer 可以分析網站所用的各項技術。