Chrome擴展程序開發

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

chrome插件源碼css

插件對應的先後端源碼html

概述

chrome擴展程序

chrome擴展程序你們應該都很熟悉了,它能夠經過腳本幫咱們完成一些快速的操做。經過插件能夠捕捉到網頁內容、標籤頁、本地存儲,或者用戶的操做行爲;它也能夠在必定程度上改變瀏覽器的UI,例如頁面上右鍵的菜單、瀏覽器右上角點擊插件logo後的彈窗,或者瀏覽器新標籤頁前端

開發原因

按照慣例,開發前多問問本身 why? how?java

why:jquery

  • 我在日常看博文時,對於一些段落想進行摘抄或者備註,又懶得複製粘貼git

how:es6

  • 一個chrome擴展程序,能夠經過鼠標右鍵的菜單,或者鍵盤快捷鍵快速保存當前頁面上選擇的文本github

  • 若是沒有選擇文本,則保存網頁連接web

  • 要有對應的後臺服務,保存 user、cliper、page (後話,本文不涉及)

  • 還要有對應的前端,以便瀏覽個人保存記錄 (後話,本文不涉及)

先上個成果圖:

chrome extension - login

chrome extension - info

chrome extension - frontend

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"]
    }
  ]
}

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

  • browser_action

    • 控制logo點擊後出現的彈窗,涵蓋相關的html/js/css

    • 在彈窗中,會進行登陸/註冊的操做,並將用戶信息保存在本地儲存中。已登陸用戶則展示基本信息

  • background

    • 在後臺持續運行,或者被事件喚醒後運行

    • 右鍵菜單的點擊和異步保存事件將在這裏觸發

  • content_scripts

    • 當前瀏覽的頁面裏運行的文件,能夠操做DOM

    • 所以,我會在這個文件裏監聽用戶的選擇事件

注:

  • 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

消息傳遞

普通的消息傳遞

經過runtimeonMessagesendMessage等方法,能夠在各個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);
};
// 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({
    // ...
  });
}
長連接

經過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) {});

舉個栗子:

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

舉栗子:

咱們在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) {});
}

而在其餘環境的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];
    }
  }
});

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

正式發佈

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

最後終於搞定,線上可見:cliper extension

學習資源

下一步?

  • 插件功能豐富化

  • 插件可在網頁上高亮展現標記的文本

  • es6 + babel重構

  • 須要使用框架嗎?


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

相關文章
相關標籤/搜索