【包教包会】Chrome拓展开发实践

首发于微信公众号《前端成长记》,写于 2019.10.18javascript

导读

有句老话说的好,好记性不如烂笔头。人生中,总有那么些东西你愿去执笔写下。css

本文旨在把整个开发的过程和遇到的问题及解决方案记录下来,但愿可以给你带来些许帮助。html

安装和源码

安装和源码前端

背景

《干货!从0开始,0成本搭建我的动态博客》 中,已经完成了动态博客的搭建。接下来,将围绕该博客,开发对应的 Chrome拓展,方便使用。java

上手开发

本文不须要前期准备,直接跟我作就行了node

功能拆分

这里主要分为几个大的功能点:git

  • 内容菜单导航,方便快速进入到博客的指定菜单页
  • 地址栏搜索,根据内容可直接在地址栏出现匹配结果的文章
  • 新文章推送,若是有文章更新则自动推送

Ⅰ.必要知识介绍github

Chrome 拓展插件 其实是由 HTML/CSS/JS/图片 等资源组成的一个 .crx 的拓展包,解压出来便可获得真正内容。web

Chrome 拓展插件 对项目结构没有要求,只须要在开发根目录下有一个 mainfest.json 便可。chrome

进入 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 (转载请注明出处:chenjiahao.xyz)