十一在家無聊時開發了這個項目。其出發點是想經過chrome插件,來保存網頁上選中的文本。後來就順手把先後端都作了(Koa2 + React):javascript
chrome插件源碼css
插件對應的先後端源碼html
chrome擴展程序你們應該都很熟悉了,它能夠經過腳本幫咱們完成一些快速的操做。經過插件能夠捕捉到網頁內容、標籤頁、本地存儲,或者用戶的操做行爲;它也能夠在必定程度上改變瀏覽器的UI,例如頁面上右鍵的菜單、瀏覽器右上角點擊插件logo後的彈窗,或者瀏覽器新標籤頁前端
按照慣例,開發前多問問本身 why? how?java
why:jquery
我在日常看博文時,對於一些段落想進行摘抄或者備註,又懶得複製粘貼git
how:es6
一個chrome擴展程序,能夠經過鼠標右鍵的菜單,或者鍵盤快捷鍵快速保存當前頁面上選擇的文本github
若是沒有選擇文本,則保存網頁連接web
要有對應的後臺服務,保存 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"] } ] }
綜上,咱們一共有三種資源文件,針對着三個運行環境:
browser_action
控制logo點擊後出現的彈窗,涵蓋相關的html/js/css
在彈窗中,會進行登陸/註冊的操做,並將用戶信息保存在本地儲存中。已登陸用戶則展示基本信息
background
在後臺持續運行,或者被事件喚醒後運行
右鍵菜單的點擊和異步保存事件將在這裏觸發
content_scripts
當前瀏覽的頁面裏運行的文件,能夠操做DOM
所以,我會在這個文件裏監聽用戶的選擇事件
注:
content_scripts
中若是沒有matches
,則擴展程序沒法正常加載,也不能經過「加載未封裝的擴展程序」來添加。若是你的content_scripts
中有js能夠針對全部頁面運行,則填寫"matches" : ["http://*/*", "https://*/*"]
便可
推薦將background
中的persistent
設置爲false
,根據事件來運行後臺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引入,而須要保持資源文件到項目目錄後再引入
雖然運行環境和繩命週期都不相同,但幸運的是,chrome爲咱們提供了一些三種JS都通用的API,能夠起到JS之間相互通信的效果。
經過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); };
// 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 }); } }); });
要使用這個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
是一個基於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) {});
舉栗子:
咱們在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商店了。
首先,你要在開發者信息中心進行登記,繳費5刀。這一步能夠參照如何成爲一名Chrome應用開發者一文來經過驗證和支付。但須要注意的是,我在嘗試時使用的帳戶爲中國google帳戶,所以徹底沒法支付,直到從新註冊了一個香港帳戶才搞定
以後,要填寫一系列的發佈信息。google對icon和banner的尺寸要求的至關嚴格。。這一步能夠參考Google Chrome 應用商店上傳擴展程序一文
最後終於搞定,線上可見:cliper extension
插件功能豐富化
插件可在網頁上高亮展現標記的文本
用es6
+ babel
重構
須要使用框架嗎?
注:本文源碼位於github倉庫:cliper-chrome,線上產品見:cliper 和 cliper extension