Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
ChenJiaH opened this issue Oct 18, 2019 · 0 comments
Open

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

ChenJiaH opened this issue Oct 18, 2019 · 0 comments
Labels
javascript toolkit 工具包相关

Comments

@ChenJiaH
Copy link
Owner

ChenJiaH commented Oct 18, 2019

首发于微信公众号《前端成长记》,写于 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)

@ChenJiaH ChenJiaH added chrome javascript toolkit 工具包相关 and removed chrome labels Oct 22, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
javascript toolkit 工具包相关
Projects
None yet
Development

No branches or pull requests

1 participant