共计 9511 个字符,预计需要花费 24 分钟才能阅读完成。
首发于微信公众号《前端成长记》,写于 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
。[[参考文档]](https://developer.chrome.com/…
-
browser_action
:如果你想让图标一直可见,那么配置该项 -
page_action
:如果你不想让图标一直可见,那么配置该项
为了让图标一直可见,我们来修改下 mainfest.json
:
{ | |
... | |
"browser_action": { | |
"default_icon": "icon.png", | |
"default_title": "McChen" | |
}, | |
... | |
} |
此时再次更新查看效果:
到这里,基础的配置开发已经完成了,接下来就是功能部分。
Ⅲ. 内容菜单导航开发
[[参考文档]](https://developer.chrome.com/…
内容导航菜单我用在两个地方:鼠标点击右上角图标的 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
内容:
[[权限参考文档]](https://developer.chrome.com/…
... | |
"permissions": ["contextMenus"] | |
... |
接下来,需要通过 API
调用去创建对应的菜单,这里需要用到常驻在后台运行的 js
才行,所以还需要修改 mainfest.json
文件:
... | |
"background": { | |
"scripts": ["background.js"] | |
}, | |
... |
然后我们新建一个 backgroud.js
文件,文件内容如下:
[[参考文档]](https://developer.chrome.com/…
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}); | |
} | |
}) |
更新后,点击鼠标右键将查看到如下内容:
至此,内容菜单导航功能已全部完成。
Ⅳ. 地址栏搜索开发
[[参考文档]](https://developer.chrome.com/…
地址栏搜索主要是通过 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
后,输入 干货
,将会看到如下内容:
至此,地址栏搜索功能已全部完成。
Ⅴ. 新文章推送开发
[[存储参考文档]](https://developer.chrome.com/…
[[推送参考文档]](https://developer.chrome.com/…
新文章推送功能,首先我们需要知道之前的最新文章是哪篇,才能做到精准推送,所以这里需要用到 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$,所以我这里就不做演示了,需要的可以自行尝试,[[发布地址]](https://chrome.google.com/web…
结尾
一个基于动态博客的 Chrome 拓展插件
就开发完了,欢迎下载使用。
如有疑问或不对之处,欢迎留言。
(完)
本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验
如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork
(转载请注明出处:https://chenjiahao.xyz)