【包教包會】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 文件,將會看到如下兩處變化:

code-img1

code-img2

你會發現你的拓展插件已經添加到右上角了,點擊右鍵時出現的第一行為 name ,點擊跳轉鏈接為 homepage_url

接下來我們為我們的拓展插件添加圖標,在 src 中新建一個名為 icon.png 的圖標,然后修改 mainfest.json 文件:

// mainfest.json
{
...
   "icons": {
    "16": "icon.png",
    "32": "icon.png",
    "48": "icon.png",
    "128": "icon.png"
  }
...
}

點擊插件開發的更新圖標,我們可以看到圖標已經加上了:

code-img3

code-img4

這里會發現,右上角的圖標為什么是置灰的呢?這里就需要聊到 browser_actionpage_action[參考文檔]

  • browser_action :如果你想讓圖標一直可見,那么配置該項
  • page_action :如果你不想讓圖標一直可見,那么配置該項

為了讓圖標一直可見,我們來修改下 mainfest.json

{
...
  "browser_action": {
    "default_icon": "icon.png",
    "default_title": "McChen"
  },
...
}

此時再次更新查看效果:

code-img5

到這里,基礎的配置開發已經完成了,接下來就是功能部分。

Ⅲ.內容菜單導航開發

[參考文檔]

內容導航菜單我用在兩個地方:鼠標點擊右上角圖標的 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>

我們更新后來看看效果,點擊右上角圖標將會看到如下的內容彈窗:

code-img6

下一步,我們來實現在網頁中按鼠標右鍵出現的菜單。

首先,你必須要配置對應的權限才能使用這個 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});
	}
})

更新后,點擊鼠標右鍵將查看到如下內容:

code-img7

至此,內容菜單導航功能已全部完成。

Ⅳ.地址欄搜索開發

[參考文檔]

地址欄搜索主要是通過 Omnibox 來實現的,我們首先需要設置關鍵字,在這里我設置成 'mc' ,修改 mainfest.json 文件:

...
{
  "omnibox": { "keyword" : "mc" }
}
...

更新后,我們在地址欄輸入 mcTab 或者 Space 鍵可看到如下內容:

code-img8

接下來我們進行接口開發,由於需要進行接口調用,所以需要配置允許請求的地址,修改 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});
			}
		});
	}
});
...

這里有幾個地方需要注意一下:

  1. onInputChanged 這方法觸發頻率高,和正常開發一樣,需要做一次函數防抖,要不然請求頻率會特別高。
  2. 這里面不允許寫 Promise ,所以我使用的 XMLHttpRequest
  3. suggestcontentdescription 字段都不允許為空,但是在事件回調里需要識別,所以我這里特意增加了一個前綴 ISSUE_NUMBER:

更新后,在地址欄輸入 mcTab 后,輸入 干貨 ,將會看到如下內容:

code-img9

至此,地址欄搜索功能已全部完成。

Ⅴ.新文章推送開發

[存儲參考文檔]

[推送參考文檔]

新文章推送功能,首先我們需要知道之前的最新文章是哪篇,才能做到精准推送,所以這里需要用到 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);
}
...

注意:由於是后台常駐,所以需要增加輪詢來判斷是否有更新,我這里設置的是一天一次

更新后,第一次我們會看到瀏覽器右下角會有推送消息如下:

code-img10

至此,新文章推送功能也已經開發完成了。

打包發布

在拓展程序頁面點擊打包擴展程序,選擇 src 作為根目錄打包即可。

將會生成 src.crxsrc.pem 兩個文件, .crx 文件就是你提交到拓展商店的資源, .pem 文件是私鑰,下次進行打包更新時需要使用。

由於打包需要 5$ ,所以我這里就不做演示了,需要的可以自行嘗試,[發布地址]

結尾

一個基於動態博客的 Chrome 拓展插件 就開發完了,歡迎下載使用。

如有疑問或不對之處,歡迎留言。

(完)


本文為原創文章,可能會更新知識點及修正錯誤,因此轉載請保留原出處,方便溯源,避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗
如果能給您帶去些許幫助,歡迎 ⭐️star 或 ✏️ fork
(轉載請注明出處:https://chenjiahao.xyz)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM