概述
chrome擴(kuò)展程序
chrome擴(kuò)展程序大家應(yīng)該都很熟悉了,它可以通過腳本幫我們完成一些快速的操作。通過插件可以捕捉到網(wǎng)頁內(nèi)容、標(biāo)簽頁、本地存儲,或者用戶的操作行為;它也可以在一定程度上改變?yōu)g覽器的UI,例如頁面上右鍵的菜單、瀏覽器右上角點擊插件logo后的彈窗,或者瀏覽器新標(biāo)簽頁
開發(fā)緣由
按照慣例,開發(fā)前多問問自己 why? how?
why:
how:
一個chrome擴(kuò)展程序,可以通過鼠標(biāo)右鍵的菜單,或者鍵盤快捷鍵快速保存當(dāng)前頁面上選擇的文本
如果沒有選擇文本,則保存網(wǎng)頁鏈接
要有對應(yīng)的后臺服務(wù),保存 user、cliper、page (后話,本文不涉及)
還要有對應(yīng)的前端,以便瀏覽我的保存記錄 (后話,本文不涉及)
先上個成果圖:



clip 有剪輯之意,因此項目命名為 cliper
這兩天終于安奈不住買了服務(wù)器,終于把網(wǎng)址部署了,也上線了chrome插件:
在項目根目錄下創(chuàng)建manifest.json文件,其中會涵蓋擴(kuò)展程序的基本信息,并指明需要的權(quán)限和資源文件
{
// 以下為必寫
"manifest_version": 2, // 必須為2,1號版本已棄用
"name": "cliper", // 擴(kuò)展程序名稱
"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",
// 根據(jù)自己使用的權(quán)限填寫
"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,后臺執(zhí)行的文件,一般只需要指定js即可。會在瀏覽器打開后全局范圍內(nèi)后臺運行
"background": {
"scripts": ["js/vendor/jquery-3.1.1.min.js", "js/background.js"],
// persistent代表“是否持久”。如果是一個單純的全局后臺js,需要一直運行,則不需配置persistent(或者為true)。當(dāng)配置為false時轉(zhuǎn)變?yōu)槭录s,依舊存在于后臺,在需要時加載,空閑時卸載
"persistent": false
},
// content_scripts,在各個瀏覽器頁面里運行的文件,可以獲取到當(dāng)前頁面的上下文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"]
}
]
}
綜上,我們一共有三種資源文件,針對著三個運行環(huán)境:
-
browser_action
-
background
在后臺持續(xù)運行,或者被事件喚醒后運行
右鍵菜單的點擊和異步保存事件將在這里觸發(fā)
-
content_scripts
注:
content_scripts中如果沒有matches,則擴(kuò)展程序無法正常加載,也不能通過“加載未封裝的擴(kuò)展程序”來添加。如果你的content_scripts中有js可以針對所有頁面運行,則填寫"matches" : ["http://*/*", "https://*/*"]即可
推薦將background中的persistent設(shè)置為false,根據(jù)事件來運行后臺js
不同運行環(huán)境JS的繩命周期
如上所述,三種JS有著三種運行環(huán)境,它們的生命周期、可操作DOM/接口也不同
content_scripts會在每個標(biāo)簽頁初始化加載的時候進(jìn)行調(diào)用,關(guān)閉頁面時卸載
內(nèi)容腳本,在每個標(biāo)簽頁下運行。雖然它可以訪問到頁面DOM,但無法訪問到這個里面里,其他JS文件創(chuàng)建的全局變量或者函數(shù)。也就是說,各個content_scripts(以及外部JS文件)之間是相互獨立的,只有:
"content_scripts": [
{
"js": [...]
}
]
js所定義的一個Array里的各個JS可以相互影響。
官方建議將后臺js配置為"persistent": false,以便在需要時加載,再次進(jìn)入空閑狀態(tài)后卸載
什么時候會讓background的資源文件加載呢?
應(yīng)用程序第一次安裝或者更新
監(jiān)聽某個事件觸發(fā)(例如chrome.runtime.onInstalled.addListener)
監(jiān)聽其他環(huán)境的JS文件發(fā)送消息(例如chrome.runtime.onMessage.addListener)
擴(kuò)展程序的其他資源文件調(diào)用了runtime.getBackgroundPage
browser_action里的資源會在彈窗打開時初始化,關(guān)閉時卸載
browser_action里定義的JS/CSS運行環(huán)境僅限于popup,并且會在每次點開彈窗的時候初始化。但是它可以調(diào)用一些chrome api,以此來和其他js進(jìn)行交互
除此以外:
不同運行環(huán)境JS之間的交互
雖然運行環(huán)境和繩命周期都不相同,但幸運的是,chrome為我們提供了一些三種JS都通用的API,可以起到JS之間相互通訊的效果。
消息傳遞
普通的消息傳遞
通過runtime的onMessage、sendMessage等方法,可以在各個JS之間傳遞并監(jiān)聽消息。舉個栗子:
在popup.js中,我們讓它初始化之后發(fā)送一個消息:
chrome.runtime.sendMessage({
method: 'showAlert'
}, function(response) {});
然后在background.js中,監(jiān)聽消息的接收,并進(jìn)行處理:
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if (message.method === 'showAlert') {
alert('showAlert');
}
});
以上代碼,會在每次打開插件彈窗的時候彈出一個Alert。
chrome.runtime的常用方法:
// 獲取當(dāng)前擴(kuò)展程序中正在運行的后臺網(wǎng)頁的 JavaScript window 對象
chrome.runtime.getBackgroundPage(function (backgroundPage) {
// backgroundPage 即 window 對象
});
// 發(fā)送消息
chrome.runtime.sendMessage(message, function(response) {
// response 代表消息回復(fù),可以接受到通過 sendResponse 方法發(fā)送的消息回復(fù)
});
// 監(jiān)聽消息
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
// message 就是你發(fā)送的 message
// sender 代表發(fā)送者,可以通過 sender.tab 判斷消息是否是從內(nèi)容腳本發(fā)出
// sendResponse 可以直接發(fā)送回復(fù),如:
sendResponse({
method: 'response',
message: 'send a response'
});
});
需要注意的是,即便你在多個JS中注冊了消息監(jiān)聽onMessage.addListener,也只有一個監(jiān)聽者能收到通過runtime.sendMessage發(fā)送出去的消息。如果需要不同的監(jiān)聽者分別監(jiān)聽消息,則需要使用chrome.tab API來指定消息接收對象
舉個栗子:
上文說過,需要在content_scripts中監(jiān)聽選擇事件,獲取選擇的文本,而對于右鍵菜單的點擊則是在background中監(jiān)聽的。那么需要把選擇的文本作為消息,發(fā)送給background,在background完成異步保存。
// content_scripts 中獲取選擇,并發(fā)送消息
// 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;
}
// 發(fā)送消息
function sendSelectionMessage(message) {
chrome.runtime.sendMessage(message, function(response) {});
}
// 監(jiān)聽鼠標(biāo)松開的事件,只有在右鍵點擊時,才會去獲取文本
window.onmouseup = function(e) {
if (!e.button === 2) {
return;
}
var message = getSelectionMessage();
sendSelectionMessage(message);
};
// background 中接收消息,監(jiān)聽右鍵菜單的點擊,并異步保存數(shù)據(jù)
// js/background.js
// 創(chuàng)建一個全局對象,來保存接收到的消息值
var selectionObj = null;
// 首先要創(chuàng)建菜單
chrome.runtime.onInstalled.addListener(function() {
chrome.contextMenus.create({
type: 'normal',
title: 'save selection',
id: 'save_selection',
// 有選擇才會出現(xiàn)
contexts: ['selection']
});
});
// 監(jiān)聽菜單的點擊
chrome.contextMenus.onClicked.addListener(function(menuItem) {
if (menuItem.menuItemId === "save_selection") {
addCliper();
}
});
// 消息監(jiān)聽,接收從 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之間的長鏈接。
信息的發(fā)送者需要制定獨特的信息類型,發(fā)送并監(jiān)聽信息:
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);
}
});
而接受者則要注冊監(jiān)聽,并判斷消息的類型:
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
});
}
});
});
要使用這個API則需要先在manifest.json中注冊:
"permissions": [
"tabs",
// ...
]
// 獲取到當(dāng)前的Tab
chrome.tabs.getCurrent(function(tab) {
// 通過 tab.id 可以拿到標(biāo)簽頁的ID
});
// 通過 queryInfo,以Array的形式篩選出符合條件的tabs
chrome.tabs.query(queryInfo, function(tabs) {})
// 精準(zhǔn)的給某個頁面的`content_scripts`發(fā)送消息
chrome.tabs.sendMessage(tabId, message, function(response) {});
舉個栗子:
在background.js中,我們獲取到當(dāng)前Tab,并發(fā)送消息:
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中,進(jìn)行消息監(jiān)聽:
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if (message.method === 'tab') {
console.log(message.message);
}
});
chrome.storage是一個基于localStorage的本地儲存,但chrome對其進(jìn)行了IO的優(yōu)化,可以儲存對象形式的數(shù)據(jù),也不會因為瀏覽器完全關(guān)閉而清空。
同樣,使用這個API需要先在manifest.json中注冊:
"permissions": [
"storage",
// ...
]
chrome.storage有兩種形式,chrome.storage.sync和chrome.storage.local:
chrome.storage.local是基于本地的儲存,而chrome.storage.sync會先判斷當(dāng)前用戶是否登錄了google賬戶,如果登錄,則會將儲存的數(shù)據(jù)通過google服務(wù)自動同步,否則,會使用chrome.storage.local僅進(jìn)行本地儲存
注:因為儲存區(qū)沒有加密,所以不應(yīng)該儲存用戶的敏感信息
API:
// 數(shù)據(jù)儲存
StorageArea.set(object items, function callback)
// 數(shù)據(jù)獲取
StorageArea.get(string or array of string or object keys, function callback)
// 數(shù)據(jù)移除
StorageArea.remove(string or array of string keys, function callback)
// 清空全部儲存
StorageArea.clear(function callback)
// 監(jiān)聽儲存的變化
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 請求異步登錄,獲取到成功的回調(diào)后,將返回的 user 對象儲存在 storage 中
chrome.storage.sync.set({user: user}, function(result) {});
}
而在其他環(huán)境的JS里,我們可以監(jiān)聽storage的變化:
// background
// js/background.js
// 一個全局的 user 對象,用來保存用戶信息,以便在異步時發(fā)生 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];
}
}
});
大體上,我們目前為止理清了三種環(huán)境下JS的不同,以及他們交流和儲存的方式。除此以外,還有popup彈窗、右鍵菜單的創(chuàng)建和使用。其實使用這些知識就足夠做出一個簡單的chrome擴(kuò)展了。
正式發(fā)布
其實我覺得整個過程中最蛋疼的一步就是把插件正式發(fā)布到chrome商店了。
最后終于搞定,線上可見:cliper extension
|