chrome extension 開(kāi)發(fā)實(shí)戰(zhàn)之 QQ 空間助手
TL;DR
背景介紹 需求分析 知識(shí)儲(chǔ)備 文檔推薦 結(jié)構(gòu)組成 組件通信 開(kāi)發(fā)調(diào)試 打包發(fā)布 問(wèn)題解決
背景介紹
本文歷時(shí)較久,2020年寫了一半,寫到迷茫,索性擱置,2021年翻出來(lái)繼續(xù)寫的;經(jīng)過(guò)了半年多的時(shí)間再看,會(huì)有一些不一樣的理解,存在部分配圖風(fēng)格不一致.但是我會(huì)確保內(nèi)容的正確和表達(dá)的一致.
因?yàn)橐彩堑谝淮螌懖寮?所以記錄這個(gè)過(guò)程,分享開(kāi)發(fā)過(guò)程和期間遇到的問(wèn)題,并做一些關(guān)鍵點(diǎn)的梳理.
所以,這并不是一篇大而全的chrome extension開(kāi)發(fā)教程,這是一篇以QQ空間助手為實(shí)戰(zhàn)來(lái)切入插件開(kāi)發(fā)但是側(cè)重講chrome extension開(kāi)發(fā)的文章.(注:文中有大量鏈接,建議點(diǎn)擊閱讀原文進(jìn)行閱讀~)
需求分析
實(shí)現(xiàn)上也很簡(jiǎn)單,只需要兩個(gè)步驟.
獲取說(shuō)說(shuō)或者留言列表 依次遍歷id刪除說(shuō)說(shuō)或者留言
知識(shí)儲(chǔ)備
JavaScript HTML + CSS
chrome devtools的使用和基本的調(diào)試能力 文檔閱讀能力
文檔推薦

接觸新的技術(shù),文檔肯定是不可少的,但是網(wǎng)上各種教程 + 文檔,魚(yú)龍混雜,我們到底應(yīng)該看教程還是看文檔.
我個(gè)人理解是,不管教程還是文檔,都可以看,但是要優(yōu)先看官方的文檔和教程,看一手的資料,因?yàn)槲铱次臋n的過(guò)程中發(fā)現(xiàn),非官方的東西(非一手文檔)信息傳達(dá)不完整,甚至有紕漏.所以盡著官方的來(lái),除非有很好的非官方教程或者其他的原因.

一篇文章教你順利入門和開(kāi)發(fā)chrome擴(kuò)展程序(插件)(文章中的代碼有筆誤,不要直接拿來(lái)用) Chrome插件中 popup,background,contantscript消息傳遞機(jī)制
官方文檔(推薦) | 官方Demo 推薦的非官方文檔 插件通信相關(guān)輔助 一篇文章教你順利入門和開(kāi)發(fā)chrome擴(kuò)展程序(插件) Chrome插件中 popup,background,contantscript消息傳遞機(jī)制 其他 Chrome插件開(kāi)發(fā)全攻略 調(diào)試相關(guān) | Debugging extensions 打包發(fā)布相關(guān) | 創(chuàng)建和發(fā)布自定義 Chrome 應(yīng)用和擴(kuò)展程序
結(jié)構(gòu)組成
UI組成


從上面的圖上來(lái)看,主要由 icon(任務(wù)欄的圖標(biāo)) 和 點(diǎn)擊之后的彈窗兩個(gè)大件組成,部分插件還會(huì)有一個(gè)專門的用于配置的頁(yè)面(這里沒(méi)用到,就不介紹了).
再加上這兩個(gè)重要的概念之后整個(gè)結(jié)構(gòu)就是這樣的

文件結(jié)構(gòu)
qzone_helper_extension
│ background.js
│ manifest.json
│ popup.html
│ popup.js
│ qq_icon.png
└─ content_script.js
manifest.json是我們最先需要了解的部分,這個(gè)文件的功能類似于package.json文件,這里定義插件的配置信息,這些信息小到插件名稱,圖標(biāo),版本號(hào)等描述信息,大到你需要申請(qǐng)的權(quán)限和各種引入的資源,入口文件等重要配置。所以這里應(yīng)該是我們最先需要了解的部分。 qq_icon.png是最不值得我們理解的部分,這就是個(gè)圖標(biāo)文件而已。 popup.html是比較直觀的一個(gè)文件,這個(gè)就是你點(diǎn)擊插件圖標(biāo)之后彈出來(lái)的界面,那個(gè)界面是html寫的,同樣的,對(duì)應(yīng)的popup.js用來(lái)處理popup頁(yè)面的交互和popup模塊和別的模塊之間的通信。 background.js和content_script.js是核心.其中,background.js可以理解為項(xiàng)目的app.js文件,而content_script.js是和網(wǎng)頁(yè)是一伙的,可以簡(jiǎn)單的理解為是我們?cè)诰W(wǎng)頁(yè)那邊的代理,幫我們做些網(wǎng)頁(yè)相關(guān)的操作.

插件各部分功能
Icon(按鈕)
在chrome插件中,這種icon按鈕是分兩種的:
page action browser action

pupop(彈出頁(yè)面)
pupop這個(gè)頁(yè)面主要承載簡(jiǎn)單的配置和信息展示功能.
這就是個(gè)普通的html文件,里面寫你的css和js邏輯.
需要注意的是,這里的js只能操作pupop里面的DOM.
回到我們的需求,我們可以在popup頁(yè)面放置一些用于觸發(fā)操作的按鈕.比如刪除說(shuō)說(shuō)按鈕
manifest.json
{
// Require
"manifest_version": 2, // 不同的manifest版本會(huì)有不同的功能
"name": "My Extension",
"version": "versionString",
// Recommended
"default_locale": "en",
"description": "A plain text description",
"icons": {...}, // icon文件路徑
// Pick one (or none)
"browser_action": {...},
"page_action": {...},
// Optional
"action": ...,
"author": ...,
"automation": ...,
"background": { // background對(duì)應(yīng)的配置
// Recommended
"persistent": false,
// Optional
"service_worker":
},
"chrome_settings_overrides": {...},
"chrome_ui_overrides": {
"bookmarks_ui": {
"remove_bookmark_shortcut": true,
"remove_button": true
}
},
"chrome_url_overrides": {...},
"commands": {...},
"content_capabilities": ...,
"content_scripts": [{...}], // content_script對(duì)應(yīng)的配置
"content_security_policy": "policyString",
"converted_from_user_script": ...,
"current_locale": ...,
"declarative_net_request": ...,
"devtools_page": "devtools.html",
"event_rules": [{...}],
"externally_connectable": {
"matches": ["*://*.example.com/*"]
},
"file_browser_handlers": [...],
"file_system_provider_capabilities": {
"configurable": true,
"multiple_mounts": true,
"source": "network"
},
"homepage_url": "http://path/to/homepage",
"import": [{"id": "aaaa"}],
"incognito": "spanning, split, or not_allowed",
"input_components": ...,
"key": "publicKey",
"minimum_chrome_version": "versionString",
"nacl_modules": [...],
"oauth2": ...,
"offline_enabled": true,
"omnibox": {
"keyword": "aString"
},
"optional_permissions": ["tabs"],
"options_page": "options.html",
"options_ui": {
"chrome_style": true,
"page": "options.html"
},
"permissions": ["tabs"], // 需要申請(qǐng)的權(quán)限
"platforms": ...,
"replacement_web_app": ...,
"requirements": {...},
"sandbox": [...],
"short_name": "Short Name",
"signature": ...,
"spellcheck": ...,
"storage": {
"managed_schema": "schema.json"
},
"system_indicator": ...,
"tts_engine": {...},
"update_url": "http://path/to/updateInfo.xml",
"version_name": "aString",
"web_accessible_resources": [...]
}
background.js
另外,從background發(fā)出去的請(qǐng)求可以跨域.
在V3的實(shí)現(xiàn)中,background引入了service worker的概念.

content_script.js
例如,點(diǎn)擊插件的圖標(biāo),然后頁(yè)面改變顏色.這種情況是不能在background.js中直接改變頁(yè)面顏色的,而是需要通過(guò)事件發(fā)送消息到content_script.js中,通過(guò)content_script來(lái)操作頁(yè)面DOM.
了解完這些我們大概知道了,我們獲取發(fā)請(qǐng)求的哪些參數(shù)包括發(fā)請(qǐng)求都可以在content_script中實(shí)現(xiàn).因?yàn)橹挥兄惪梢圆僮黜?yè)面的DOM.
組件通信
chrome extension的通信是通過(guò)事件來(lái)進(jìn)行的,通信的內(nèi)容是有效的JSON對(duì)象.共有三種通信方式:
Simple one-time requests (就像短連接) Long-lived connections (就像長(zhǎng)連接) Cross-extension messaging (多個(gè)插件間通信)
插件內(nèi)的通信分為這幾種:
content script => background content script => popup background => content script background => popup popup => content script popup => background
chrome.runtime.sendMessage({greeting: "hello"}, function(response) {
console.log(response.farewell);
});
content script發(fā)送的事件popup和background都可以收到,所以再發(fā)送的時(shí)候需要加上發(fā)給誰(shuí)的標(biāo)識(shí),然后收到消息的時(shí)候處理 popup => background的通信其實(shí)也可以通過(guò)chrome.extension.getBackgroundPage()來(lái)獲取到background的所有方法,直接調(diào)用
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, {greeting: "hello"}, function(response) {
console.log(response.farewell);
});
});
因?yàn)榭赡艽蜷_(kāi)了多個(gè)同一個(gè)url的tab,所以需要區(qū)分tab. 通過(guò)chrome.tabs.sendMessage發(fā)送的事件只有指定頁(yè)面的content可以收到
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
console.log(sender.tab ?
"from a content script:" + sender.tab.url :
"from the extension");
if (request.greeting == "hello")
sendResponse({farewell: "goodbye"});
}
);

加上通信的部分,我們大概清楚了,我們?cè)趐opup中觸發(fā)操作,然后popup發(fā)送事件給content_script,content_script開(kāi)始獲取請(qǐng)求參數(shù),請(qǐng)求數(shù)據(jù)列表,然后遍歷數(shù)據(jù)做刪除操作.整個(gè)過(guò)程大概就這樣,沒(méi)用到background.

開(kāi)發(fā)調(diào)試
開(kāi)發(fā)時(shí)候如何運(yùn)行插件

如何打開(kāi)控制臺(tái)
打開(kāi)background的控制臺(tái)
在chrome://extensions/頁(yè)面,點(diǎn)擊對(duì)應(yīng)的插件背景頁(yè)三個(gè)字即可打開(kāi)background的控制臺(tái)

打開(kāi)popup的控制臺(tái)
點(diǎn)擊icon在彈出的popup頁(yè)面上右鍵,然后點(diǎn)擊檢查,就可以打開(kāi)popup的控制臺(tái).需要注意的是popup頁(yè)面一旦關(guān)閉,控制臺(tái)也會(huì)隨之關(guān)閉.

打開(kāi)content script的控制臺(tái)
content script的控制臺(tái)其實(shí)就是tab頁(yè)的控制臺(tái),但是需要切換一下
如何debug
https://developer.chrome.com/extensions/tut_debugging
打包發(fā)布
更多詳情可以參考文檔創(chuàng)建和發(fā)布自定義 Chrome 應(yīng)用和擴(kuò)展程序



問(wèn)題解決
chrome.tabs.query得到空的tabs
var activeTabId;
chrome.tabs.onActivated.addListener(function(activeInfo) {
activeTabId = activeInfo.tabId;
});
// https://bugs.chromium.org/p/chromium/issues/detail?id=462939
function getActiveTab(callback) {
chrome.tabs.query({currentWindow: true, active: true}, function (tabs) {
let tab = tabs[0];
if (tab) {
callback(tab.id);
} else {
chrome.tabs.get(activeTabId, function (tab) {
if (tab) {
callback(tab.id);
} else {
console.log('No active tab identified.');
}
});
}
});
}
讓插件只在特定頁(yè)面可用功能實(shí)現(xiàn)
這個(gè)功能的實(shí)現(xiàn)思路比較多,這里講兩種
一種是通過(guò)監(jiān)聽(tīng)conect事件,在事件的處理方法中setIcon和初始化對(duì)應(yīng)的popup頁(yè)面.Vimium是這么做的,可以看這里.
還有一些利用chrome的onPageChanged事件的規(guī)則來(lái)實(shí)現(xiàn),這種處理只有在符合規(guī)則的時(shí)候才展示popup頁(yè)面.
chrome.runtime.onInstalled.addListener(function() {
chrome.declarativeContent.onPageChanged.removeRules(undefined, function() {
chrome.declarativeContent.onPageChanged.addRules([{
conditions: [
new chrome.declarativeContent.PageStateMatcher({
pageUrl: {
hostContains: 'qzone.qq.com'
}
})
],
actions: [new chrome.declarativeContent.ShowPageAction()]
}]);
});
});
操作插件(點(diǎn)擊或者別的操作)沒(méi)有響應(yīng)



評(píng)論
圖片
表情
