【包教包會】Chrome拓展開發實踐
首發於微信公眾號《前端成長記》,寫於 2019.10.18
導讀
有句老話說的好,好記性不如爛筆頭。人生中,總有那麼些東西你願去執筆寫下。
本文旨在把整個開發的過程和遇到的問題及解決方案記錄下來,希望能夠給你帶來些許幫助。
安裝和原始碼
安裝和原始碼
背景
在 《乾貨!從0開始,0成本搭建個人動態部落格》 中,已經完成了動態部落格的搭建。接下來,將圍繞該部落格,開發對應的 Chrome拓展
,方便使用。
上手開發
本文不需要前期準備,直接跟我做就好了
功能拆分
這裡主要分為幾個大的功能點:
- 內容選單導航,方便快速進入到部落格的指定選單頁
- 位址列搜尋,根據內容可直接在位址列出現匹配結果的文章
- 新文章推送,如果有文章更新則自動推送
Ⅰ.必要知識介紹
Chrome 拓展外掛
實際上是由 HTML/CSS/JS/圖片
等資源組成的一個 .crx
的拓展包,解壓出來即可得到真正內容。
Chrome 拓展外掛
對專案結構沒有要求,只需要在開發根目錄下有一個 mainfest.json
即可。
進入 Chrome 拓展程式
頁面,開啟 開發者模式
開始我們的開發之路。
Ⅱ.基礎配置開發
首先,新建一個 src
目錄作為外掛的檔案目錄,然後新建一個 mainfest.json
檔案,檔案內容如下:
// mainfest.json { // 外掛名稱 "name": "McChen", // 外掛版本號 "version": "0.0.1", // 外掛描述 "description": "Chrome Extension for McChen.", // 外掛主頁 "homepage_url": "https://chenjiahao.xyz", // 版本必須指定為2 "manifest_version": 2 }
然後開啟 Chrome
拓展程式頁面,點選 載入已解壓的拓展程式 按鈕,選擇上面新建的 src
檔案,將會看到如下兩處變化:
你會發現你的拓展外掛已經新增到右上角了,點選右鍵時出現的第一行為 name
,點選跳轉連結為 homepage_url
。
接下來我們為我們的拓展外掛新增圖示,在 src
中新建一個名為 icon.png
的圖示,然後修改 mainfest.json
檔案:
// mainfest.json { ... "icons": { "16": "icon.png", "32": "icon.png", "48": "icon.png", "128": "icon.png" } ... }
點選外掛開發的更新圖示,我們可以看到圖示已經加上了:
這裡會發現,右上角的圖示為什麼是置灰的呢?這裡就需要聊到 browser_action
和 page_action
。[參考文件]
browser_action
:如果你想讓圖示一直可見,那麼配置該項page_action
:如果你不想讓圖示一直可見,那麼配置該項
為了讓圖示一直可見,我們來修改下 mainfest.json
:
{
...
"browser_action": {
"default_icon": "icon.png",
"default_title": "McChen"
},
...
}
此時再次更新檢視效果:
到這裡,基礎的配置開發已經完成了,接下來就是功能部分。
Ⅲ.內容選單導航開發
[參考文件]
內容導航選單我用在兩個地方:滑鼠點選右上角圖示的 Popup
和網頁中按滑鼠右鍵出現的選單。
先看看滑鼠點選右上角圖示 Popup
的,給 mainfest.json
增加 default_popup
就是 popup
展示的頁面內容了。
{
...
"browser_action": {
"default_icon": "icon.png",
"default_title": "McChen",
"default_popup": "popup.html"
},
...
}
新建一個 popup.html
檔案,內容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<title>McChen</title>
<meta charset="utf-8"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style type="text/css">
#McChen-container { padding: 4px 0; margin: 0; width: 80px; user-select: none; overflow: hidden; text-align: center; background-color: #f6f8fc;}
.McChen-item_a { position: relative; display: block; font-size: 14px; color: #283039; transition: all 0.2s; line-height: 28px; text-decoration: none; white-space: nowrap; text-indent: 16px;}
.McChen-item_a:before { position: absolute; top: 50%; margin-top: -14px; font-size: 16px; line-height: 28px;}
.McChen-item_a:after { position: absolute; top: 50%; margin-top: -14px; font-size: 16px; line-height: 28px;}
.McChen-item_a + .McChen-item_a { border-top: 1px solid #f0f2f5;}
.McChen-item_a:hover { color: #0074ff;}
.McChen-item_a:nth-child(1):before { content: '·'; left: 4px;}
.McChen-item_a:nth-child(2):before { content: '··'; left: 2px;}
.McChen-item_a:nth-child(3):before { content: '···'; left: 0;}
.McChen-item_a:nth-child(4):before { content: '····'; left: -2px;}
.McChen-item_a:nth-child(5):before { content: '····'; margin-top: -16px; left: -2px;}
.McChen-item_a:nth-child(5):after { content: '·'; margin-top: -12px; left: -2px;}
.McChen-item_a:nth-child(6):before { content: '····'; margin-top: -16px; left: -2px;}
.McChen-item_a:nth-child(6):after { content: '··'; margin-top: -12px; left: -2px;}
.McChen-item_a:nth-child(7):before { content: '····'; margin-top: -16px; left: -2px;}
.McChen-item_a:nth-child(7):after { content: '···'; margin-top: -12px; left: -2px;}
</style>
</head>
<body id="McChen-container">
<a class="McChen-item_a" href="https://chenjiahao.xyz" target="_blank">主頁</a>
<a class="McChen-item_a" href="https://chenjiahao.xyz/blog/#/archives" target="_blank">部落格</a>
<a class="McChen-item_a" href="https://chenjiahao.xyz/blog/#/labels" target="_blank">標籤</a>
<a class="McChen-item_a" href="https://chenjiahao.xyz/blog/#/links" target="_blank">友鏈</a>
<a class="McChen-item_a" href="https://chenjiahao.xyz/blog/#/about" target="_blank">關於</a>
<a class="McChen-item_a" href="https://chenjiahao.xyz/blog/#/board" target="_blank">留言</a>
<a class="McChen-item_a" href="https://chenjiahao.xyz/blog/#/search" target="_blank">搜尋</a>
</body>
</html>
我們更新後來看看效果,點選右上角圖示將會看到如下的內容彈窗:
下一步,我們來實現在網頁中按滑鼠右鍵出現的選單。
首先,你必須要配置對應的許可權才能使用這個 API
,還需要配置修改 mainfest.json
內容:
[許可權參考文件]
...
"permissions": [
"contextMenus"
]
...
接下來,需要通過 API
呼叫去建立對應的選單,這裡需要用到常駐在後臺執行的 js
才行,所以還需要修改 mainfest.json
檔案:
...
"background": {
"scripts": [
"background.js"
]
},
...
然後我們新建一個 backgroud.js
檔案,檔案內容如下:
[參考文件]
chrome.contextMenus.create({
id: 'McChen',
title: 'McChen',
contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
});
chrome.contextMenus.create({
id: 'home',
title: '主頁',
parentId: 'McChen', // 右鍵選單項的父選單項ID。指定父選單項將會使此選單項成為父選單項的子選單
contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
});
chrome.contextMenus.create({
id: 'archives',
title: '部落格',
parentId: 'McChen', // 右鍵選單項的父選單項ID。指定父選單項將會使此選單項成為父選單項的子選單
contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
});
chrome.contextMenus.create({
id: 'labels',
title: '標籤',
parentId: 'McChen', // 右鍵選單項的父選單項ID。指定父選單項將會使此選單項成為父選單項的子選單
contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
});
chrome.contextMenus.create({
id: 'links',
title: '友鏈',
parentId: 'McChen', // 右鍵選單項的父選單項ID。指定父選單項將會使此選單項成為父選單項的子選單
contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
});
chrome.contextMenus.create({
id: 'about',
title: '關於',
parentId: 'McChen', // 右鍵選單項的父選單項ID。指定父選單項將會使此選單項成為父選單項的子選單
contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
});
chrome.contextMenus.create({
id: 'board',
title: '留言',
parentId: 'McChen', // 右鍵選單項的父選單項ID。指定父選單項將會使此選單項成為父選單項的子選單
contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
});
chrome.contextMenus.create({
id: 'search',
title: '搜尋',
parentId: 'McChen', // 右鍵選單項的父選單項ID。指定父選單項將會使此選單項成為父選單項的子選單
contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
});
// 監聽選單點選事件
chrome.contextMenus.onClicked.addListener(function (info, tab) {
if (info.menuItemId === 'home') {
chrome.tabs.create({url: 'https://chenjiahao.xyz'});
} else {
chrome.tabs.create({url: 'https://chenjiahao.xyz/blog/#/' + info.menuItemId});
}
})
更新後,點選滑鼠右鍵將檢視到如下內容:
至此,內容選單導航功能已全部完成。
Ⅳ.位址列搜尋開發
[參考文件]
位址列搜尋主要是通過 Omnibox
來實現的,我們首先需要設定關鍵字,在這裡我設定成 'mc' ,修改 mainfest.json
檔案:
...
{
"omnibox": { "keyword" : "mc" }
}
...
更新後,我們在位址列輸入 mc
按 Tab
或者 Space
鍵可看到如下內容:
接下來我們進行介面開發,由於需要進行介面呼叫,所以需要配置允許請求的地址,修改 mainfest.json
檔案:
...
{
"permissions": [
"contextMenus",
// 允許請求全部https
"https://*/"
],
}
...
然後修改 background.js
檔案內容:
...
let timer = '';
chrome.omnibox.onInputChanged.addListener((text, suggest) => {
if (timer) {
clearTimeout(timer)
timer = ''
} else {
timer = setTimeout(() => {
if (text.length > 1) {
const xhr = new XMLHttpRequest();
xhr.open("POST", "https://api.artfe.club/transfer/github", true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
const list = JSON.parse(xhr.responseText).data.search.nodes;
if (list.length) {
suggest(list.map(_ => ({content: 'ISSUE_NUMBER:' + _.number, description: '文章 - ' + _.title})))
} else {
suggest([
{content: 'none', description: '無相關結果'}
])
}
}
};
xhr.send('query=' + query);
} else {
suggest([
{content: 'none', description: '查詢中,請稍後...'}
])
}
}, 300)
}
});
// 當選中建議內容時觸發
chrome.omnibox.onInputEntered.addListener((text) => {
if (text.startsWith('ISSUE_NUMBER:')) {
const number = text.substr(13)
chrome.tabs.query({active: true, currentWindow: true}, function (tabs) {
if (tabs.length) {
const tabId = tabs[0].id;
const url = 'https://chenjiahao.xyz/blog/#/archives/' + number;
chrome.tabs.update(tabId, {url: url});
}
});
}
});
...
這裡有幾個地方需要注意一下:
onInputChanged
這方法觸發頻率高,和正常開發一樣,需要做一次函式防抖,要不然請求頻率會特別高。- 這裡面不允許寫
Promise
,所以我使用的XMLHttpRequest
suggest
中content
和description
欄位都不允許為空,但是在事件回撥裡需要識別,所以我這裡特意增加了一個字首ISSUE_NUMBER:
更新後,在位址列輸入 mc
按 Tab
後,輸入 乾貨
,將會看到如下內容:
至此,位址列搜尋功能已全部完成。
Ⅴ.新文章推送開發
[儲存參考文件]
[推送參考文件]
新文章推送功能,首先我們需要知道之前的最新文章是哪篇,才能做到精準推送,所以這裡需要用到 Storage
,也就是儲存功能。存下最新文章的 ID
,輪詢最新文章,如果有更新,則存下最新文章的 ID
並且呼叫推送的 API
。所以,我們需要先增加許可權配置,修改 mainfest.json
檔案:
...
"permissions": [
"storage",
"contextMenus",
"notifications",
"https://*/"
],
...
然後修改 'background.js' 檔案內容:
...
getLatestNumber();
chrome.storage.sync.get({LATEST_TIMER: 0}, function (items) {
if (items.LATEST_TIMER) {
clearInterval(items.LATEST_TIMER)
}
const LATEST_TIMER = setInterval(() => {
getLatestNumber()
}, 1000 * 60 * 60 *24)
chrome.storage.sync.set({LATEST_TIMER: LATEST_TIMER})
});
function getLatestNumber () {
const query = `query {
repository(owner: "ChenJiaH", name: "blog") {
issues(orderBy: {field: CREATED_AT, direction: DESC}, labels: null, first: 1, after: null) {
nodes {
title
number
}
}
}
}`;
const xhr = new XMLHttpRequest();
xhr.open("POST", "https://api.artfe.club/transfer/github", true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
const list = JSON.parse(xhr.responseText).data.repository.issues.nodes;
if (list.length) {
const title = list[0].title;
const ISSUE_NUMBER = list[0].number;
chrome.storage.sync.get({ISSUE_NUMBER: 0}, function(items) {
if (items.ISSUE_NUMBER !== ISSUE_NUMBER) {
chrome.storage.sync.set({ISSUE_NUMBER: ISSUE_NUMBER}, function() {
chrome.notifications.create('McChen', {
type: 'basic',
iconUrl: 'icon.png',
title: '新文章釋出通知',
message: title
});
chrome.notifications.onClicked.addListener(function (notificationId) {
if (notificationId === 'McChen') {
chrome.tabs.create({url: 'https://chenjiahao.xyz/blog/#/archives/' + ISSUE_NUMBER});
}
})
});
}
});
}
}
};
xhr.send('query=' + query);
}
...
注意:由於是後臺常駐,所以需要增加輪詢來判斷是否有更新,我這裡設定的是一天一次
更新後,第一次我們會看到瀏覽器右下角會有推送訊息如下:
至此,新文章推送功能也已經開發完成了。
打包釋出
在拓展程式頁面點選打包擴充套件程式,選擇 src
作為根目錄打包即可。
將會生成 src.crx
和 src.pem
兩個檔案, .crx
檔案就是你提交到拓展商店的資源, .pem
檔案是私鑰,下次進行打包更新時需要使用。
由於打包需要 5$ ,所以我這裡就不做演示了,需要的可以自行嘗試,[釋出地址]
結尾
一個基於動態部落格的 Chrome 拓展外掛
就開發完了,歡迎下載使用。
如有疑問或不對之處,歡迎留言。
(完)
本文為原創文章,可能會更新知識點及修正錯誤,因此轉載請保留原出處,方便溯源,避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗
如果能給您帶去些許幫助,歡迎 ⭐️star 或 ✏️ fork
(轉載請註明出處:https://chenjiahao.xyz)