Chrome 擴展程序開發

AnyaUJVI 8年前發布 | 75K 次閱讀 軟件開發 Chrome插件/擴展

Chrome擴展程序開發

十一在家無聊時開發了這個項目。其出發點是想通過chrome插件,來保存網頁上選中的文本。后來就順手把前后端都做了(Koa2 + React):

chrome插件源碼

插件對應的前后端源碼

概述

Chrome擴展程序大家應該都很熟悉了,它可以通過腳本幫我們完成一些快速的操作。通過插件可以捕捉到網頁內容、標簽頁、本地存儲,或者用戶的操作行為;它也可以在一定程度上改變瀏覽器的UI,例如頁面上右鍵的菜單、瀏覽器右上角點擊插件logo后的彈窗,或者瀏覽器新標簽頁

開發緣由

按照慣例,開發前多問問自己 why? how?

why:

  • 我在平常看博文時,對于一些段落想進行摘抄或者備注,又懶得復制粘貼

how:

  • 一個chrome擴展程序,可以通過鼠標右鍵的菜單,或者鍵盤快捷鍵快速保存當前頁面上選擇的文本
  • 如果沒有選擇文本,則保存網頁鏈接
  • 要有對應的后臺服務,保存 user、cliper、page (后話,本文不涉及)
  • 還要有對應的前端,以便瀏覽我的保存記錄 (后話,本文不涉及)

先上個成果圖:

clip 有剪輯之意,因此項目命名為 cliper

這兩天終于安奈不住買了服務器,終于把網址部署了,也上線了chrome插件:

manifest.json

在項目根目錄下創建 manifest.json 文件,其中會涵蓋擴展程序的基本信息,并指明需要的權限和資源文件

{
  // 以下為必寫
  "manifest_version": 2, // 必須為2,1號版本已棄用
  "name": "cliper", // 擴展程序名稱
  "version": "0.01", // 版本號

// 以下為選填

// 推薦 "description": "描述", "icons": { "16": "icons/icon_16.png", "48": "icons/icon_48.png", "64": "icons/icon_64.png", "128": "icons/icon_128.png" }, "author": "ecmadao",

// 根據自己使用的權限填寫 "permissions": [ // 例如 "tab", "storage", // 如果會在js中請求外域API或者資源,則要把外域鏈接加入 "http://localhost:5000/*" ],

// options_page,指右鍵點擊右上角里的插件logo時,彈出列表中的“選項”是否可點,以及在可以點擊時,左鍵點擊后打開的頁面 "options_page": "view/options.html",

// browser_action,左鍵點擊右上角插件logo時,彈出的popup框。不填此項則點擊logo不會有用 "browser_action": { "default_icon": { "38": "icons/icon_38.png" }, "default_popup": "view/popup.html", // popup頁面,其實就是普通的html "default_title" : "保存到cliper" },

// background,后臺執行的文件,一般只需要指定js即可。會在瀏覽器打開后全局范圍內后臺運行 "background": { "scripts": ["js/vendor/jquery-3.1.1.min.js", "js/background.js"], // persistent代表“是否持久”。如果是一個單純的全局后臺js,需要一直運行,則不需配置persistent(或者為true)。當配置為false時轉變為事件js,依舊存在于后臺,在需要時加載,空閑時卸載 "persistent": false },

// content_scripts,在各個瀏覽器頁面里運行的文件,可以獲取到當前頁面的上下文DOM "content_scripts": [ { // matches 匹配 content_scripts 可以在哪些頁面運行 "matches" : ["http://*/*", "https://*/*"], "js": ["js/vendor/jquery-3.1.1.min.js", "js/vendor/keyboard.min.js", "js/selection.js", "js/notification.js"], "css": ["css/notification.css"] } ] }</code></pre>

綜上,我們一共有三種資源文件,針對著三個運行環境:

  • browser_action
    • 控制logo點擊后出現的彈窗,涵蓋相關的html/js/css
    • 在彈窗中,會進行登錄/注冊的操作,并將用戶信息保存在本地儲存中。已登錄用戶則展現基本信息
    </li>
  • background
    • 在后臺持續運行,或者被事件喚醒后運行
    • 右鍵菜單的點擊和異步保存事件將在這里觸發
    • </ul> </li>
    • content_scripts
      • 當前瀏覽的頁面里運行的文件,可以操作DOM
      • 因此,我會在這個文件里監聽用戶的選擇事件
      • </ul> </li> </ul>

        注:

        • content_scripts 中如果沒有 matches ,則擴展程序無法正常加載,也不能通過“加載未封裝的擴展程序”來添加。如果你的 content_scripts 中有js可以針對所有頁面運行,則填寫 "matches" : ["http://*/*", "https://*/*"] 即可
        • 推薦將 background 中的 persistent 設置為 false ,根據事件來運行后臺js

        不同運行環境JS的繩命周期

        如上所述,三種JS有著三種運行環境,它們的生命周期、可操作DOM/接口也不同

        content_scripts

        content_scripts 會在每個標簽頁初始化加載的時候進行調用,關閉頁面時卸載

        內容腳本,在每個標簽頁下運行。雖然它可以訪問到頁面DOM,但無法訪問到這個里面里,其他JS文件創建的全局變量或者函數。也就是說,各個 content_scripts (以及外部JS文件)之間是相互獨立的,只有:

        "content_scripts": [
          {
            "js": [...]
          }
        ]

        js 所定義的一個Array里的各個JS可以相互影響。

        background

        官方建議將后臺js配置為 "persistent": false ,以便在需要時加載,再次進入空閑狀態后卸載

        什么時候會讓 background 的資源文件加載呢?

        • 應用程序第一次安裝或者更新
        • 監聽某個事件觸發(例如 chrome.runtime.onInstalled.addListener )
        • 監聽其他環境的JS文件發送消息(例如 chrome.runtime.onMessage.addListener )
        • 擴展程序的其他資源文件調用了 runtime.getBackgroundPage

        browser_action

        browser_action 里的資源會在彈窗打開時初始化,關閉時卸載

        browser_action 里定義的JS/CSS運行環境僅限于popup,并且會在每次點開彈窗的時候初始化。但是它可以調用一些 chrome api ,以此來和其他js進行交互

        除此以外:

        • browser_action 的HTML文件里使用的JS,不能直接以 <script></script> 的形式行內寫入HTML里,需要獨立成JS文件再引入
        • 如果有其他第三方依賴,比如 jQuery 等文件,也無法通過CDN引入,而需要保持資源文件到項目目錄后再引入

        不同運行環境JS之間的交互

        雖然運行環境和繩命周期都不相同,但幸運的是,chrome為我們提供了一些三種JS都通用的API,可以起到JS之間相互通訊的效果。

        chrome.runtime

        消息傳遞

        普通的消息傳遞

        通過 runtime 的 onMessage 、 sendMessage 等方法,可以在各個JS之間傳遞并監聽消息。舉個栗子:

        在 popup.js 中,我們讓它初始化之后發送一個消息:

        chrome.runtime.sendMessage({
          method: 'showAlert'
        }, function(response) {});

        然后在 background.js 中,監聽消息的接收,并進行處理:

        chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
          if (message.method === 'showAlert') {
            alert('showAlert');
          }
        });

        以上代碼,會在每次打開插件彈窗的時候彈出一個Alert。

        chrome.runtime 的常用方法:

        // 獲取當前擴展程序中正在運行的后臺網頁的 JavaScript window 對象
        chrome.runtime.getBackgroundPage(function (backgroundPage) {
          // backgroundPage 即 window 對象
        });
        // 發送消息
        chrome.runtime.sendMessage(message, function(response) {
          // response 代表消息回復,可以接受到通過 sendResponse 方法發送的消息回復
        });
        // 監聽消息
        chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
          // message 就是你發送的 message
          // sender 代表發送者,可以通過 sender.tab 判斷消息是否是從內容腳本發出
          // sendResponse 可以直接發送回復,如:
          sendResponse({
            method: 'response',
            message: 'send a response'
          });
        });

        需要注意的是,即便你在多個JS中注冊了消息監聽 onMessage.addListener ,也只有一個監聽者能收到通過 runtime.sendMessage 發送出去的消息。如果需要不同的監聽者分別監聽消息,則需要使用 chrome.tab API來指定消息接收對象

        舉個栗子:

        上文說過,需要在 content_scripts 中監聽選擇事件,獲取選擇的文本,而對于右鍵菜單的點擊則是在 background 中監聽的。那么需要把選擇的文本作為消息,發送給 background ,在 background 完成異步保存。

        // content_scripts 中獲取選擇,并發送消息
        // js/selection.js

        // 獲取選擇的文本 function getSelectedText() { if (window.getSelection) { return window.getSelection().toString(); } else if (document.getSelection) { return document.getSelection(); } else if (document.selection) { return document.selection.createRange().text; } } // 組建信息 function getSelectionMessage() { var text = getSelectedText(); var title = document.title; var url = window.location.href; var data = { text: text, title: title, url: url }; var message = { method: 'get_selection', data: data } return message; } // 發送消息 function sendSelectionMessage(message) { chrome.runtime.sendMessage(message, function(response) {}); } // 監聽鼠標松開的事件,只有在右鍵點擊時,才會去獲取文本 window.onmouseup = function(e) { if (!e.button === 2) { return; } var message = getSelectionMessage(); sendSelectionMessage(message); };</code></pre>

        // background 中接收消息,監聽右鍵菜單的點擊,并異步保存數據
        // js/background.js

        // 創建一個全局對象,來保存接收到的消息值 var selectionObj = null;

        // 首先要創建菜單 chrome.runtime.onInstalled.addListener(function() { chrome.contextMenus.create({ type: 'normal', title: 'save selection', id: 'save_selection', // 有選擇才會出現 contexts: ['selection'] }); }); // 監聽菜單的點擊 chrome.contextMenus.onClicked.addListener(function(menuItem) { if (menuItem.menuItemId === "save_selection") { addCliper(); } });

        // 消息監聽,接收從 content_scripts 傳遞來的消息,并保存在一個全局對象中 chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) { if (message.method === 'get_selection') { selectionObj = message.data; } });

        // 異步保存 function addCliper() { $.ajax({ // ... }); }</code></pre>

        長鏈接

        通過 chrome.runtime.connect (或者 chrome.tabs.connect )可以建立起不同類型JS之間的長鏈接。

        信息的發送者需要制定獨特的信息類型,發送并監聽信息:

        var port = chrome.runtime.connect({type: "connection"});
        port.postMessage({
          method: "add",
          datas: [1, 2, 3]
        });
        port.onMessage.addListener(function(msg) {
          if (msg.method === "answer") {
            console.log(msg.data);
          }
        });

        而接受者則要注冊監聽,并判斷消息的類型:

        chrome.runtime.onConnect.addListener(function(port) {
          console.assert(port.type == "connection");
          port.onMessage.addListener(function(msg) {
            if (msg.method == "add") {
              var result = msg.datas.reduce(function(previousValue, currentValue, index, array){
              return previousValue + currentValue;
          });
              port.postMessage({
                method: "answer",
                data: result
              });
            }
          });
        });

        chrome.tabs

        要使用這個API則需要先在 manifest.json 中注冊:

        "permissions": [
          "tabs",
          // ...
        ]
        // 獲取到當前的Tab
        chrome.tabs.getCurrent(function(tab) {
          // 通過 tab.id 可以拿到標簽頁的ID
        });

        // 通過 queryInfo,以Array的形式篩選出符合條件的tabs chrome.tabs.query(queryInfo, function(tabs) {})

        // 精準的給某個頁面的content_scripts發送消息 chrome.tabs.sendMessage(tabId, message, function(response) {});</code></pre>

        舉個栗子:

        在 background.js 中,我們獲取到當前Tab,并發送消息:

        chrome.tabs.getCurrent(function(tab) {
          chrome.tabs.sendMessage(tab.id, {
            method: 'tab',
            message: 'get active tab'
          }, function(response) {});
        });
        // 或者
        chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
          chrome.tabs.sendMessage(tabs[0].id, {
            method: 'tab',
            message: 'get active tab'
          }, function(response) {
          });
        });

        然后在 content_scripts 中,進行消息監聽:

        chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
          if (message.method === 'tab') {
            console.log(message.message);
          }
        });

        chrome.storage

        chrome.storage 是一個基于 localStorage 的本地儲存,但chrome對其進行了IO的優化,可以儲存對象形式的數據,也不會因為瀏覽器完全關閉而清空。

        同樣,使用這個API需要先在 manifest.json 中注冊:

        "permissions": [
          "storage",
          // ...
        ]

        chrome.storage 有兩種形式, chrome.storage.sync 和 chrome.storage.local :

        chrome.storage.local 是基于本地的儲存,而 chrome.storage.sync 會先判斷當前用戶是否登錄了google賬戶,如果登錄,則會將儲存的數據通過google服務自動同步,否則,會使用 chrome.storage.local 僅進行本地儲存

        注:因為儲存區沒有加密,所以不應該儲存用戶的敏感信息

        API:

        // 數據儲存
        StorageArea.set(object items, function callback)

        // 數據獲取 StorageArea.get(string or array of string or object keys, function callback)

        // 數據移除 StorageArea.remove(string or array of string keys, function callback)

        // 清空全部儲存 StorageArea.clear(function callback)

        // 監聽儲存的變化 chrome.storage.onChanged.addListener(function(changes, namespace) {});</code></pre>

        舉栗子:

        我們在 browser_action 完成了用戶的登錄/注冊操作,將部分用戶信息儲存在 storage 中。每次初始化時,都會檢查是否有儲存,沒有的話則需要用戶登錄,成功后再添加:

        // browser_action
        // js.popup.js

        chrome.storage.sync.get('user', function(result) { // 通過 result.user 獲取到儲存的 user 對象 result && setPopDOM(result.user); });

        function setPopDOM(user) { if (user && user.userId) { // show user UI } else { // show login UI } };

        document.getElementById('login').onclick = function() { // login user.. // 通過 ajax 請求異步登錄,獲取到成功的回調后,將返回的 user 對象儲存在 storage 中 chrome.storage.sync.set({user: user}, function(result) {}); }</code></pre>

        而在其他環境的JS里,我們可以監聽 storage 的變化:

        // background
        // js/background.js

        // 一個全局的 user 對象,用來保存用戶信息,以便在異步時發生 userId var user = null;

        chrome.storage.onChanged.addListener(function(changes, namespace) { for (key in changes) { if (key === 'user') { console.log('user storage changed!'); user = changes[key]; } } });</code></pre>

        大體上,我們目前為止理清了三種環境下JS的不同,以及他們交流和儲存的方式。除此以外,還有popup彈窗、右鍵菜單的創建和使用。其實使用這些知識就足夠做出一個簡單的chrome擴展了。

        正式發布

        其實我覺得整個過程中最蛋疼的一步就是把插件正式發布到chrome商店了。

        最后終于搞定,線上可見: cliper extension

        學習資源

        注:本文源碼位于 github倉庫:cliper-chrome ,線上產品見: clipercliper extension

        下一步?

        • 插件功能豐富化
        • 插件可在網頁上高亮展示標記的文本
        • 用 es6 + babel 重構
        • 需要使用框架嗎?

         

         

 本文由用戶 AnyaUJVI 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!