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

导读

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

本文旨在把整个开发的过程和遇到的问题及解决方案记录下来,希望能够给你带来些许帮助。

安装和源码

安装和源码

背景

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

上手开发

本文不需要前期准备,直接跟我做就好了

功能拆分

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

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

Ⅰ.必要知识介绍

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

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

进入 Chrome 拓展程序 页面,打开 开发者模式 开始我们的开发之路。

Ⅱ.基础配置开发

首先,新建一个 src 目录作为插件的文件目录,然后新建一个 mainfest.json 文件,文件内容如下:

  1. // mainfest.json
  2. {
  3. // 插件名称
  4. "name": "McChen",
  5. // 插件版本号
  6. "version": "0.0.1",
  7. // 插件描述
  8. "description": "Chrome Extension for McChen.",
  9. // 插件主页
  10. "homepage_url": "https://chenjiahao.xyz",
  11. // 版本必须指定为2
  12. "manifest_version": 2
  13. }

然后打开 Chrome 拓展程序页面,点击 加载已解压的拓展程序 按钮,选择上面新建的 src 文件,将会看到如下两处变化:

你会发现你的拓展插件已经添加到右上角了,点击右键时出现的第一行为 name ,点击跳转链接为 homepage_url

接下来我们为我们的拓展插件添加图标,在 src 中新建一个名为 icon.png 的图标,然后修改 mainfest.json 文件:

  1. // mainfest.json
  2. {
  3. ...
  4. "icons": {
  5. "16": "icon.png",
  6. "32": "icon.png",
  7. "48": "icon.png",
  8. "128": "icon.png"
  9. }
  10. ...
  11. }

点击插件开发的更新图标,我们可以看到图标已经加上了:

这里会发现,右上角的图标为什么是置灰的呢?这里就需要聊到 browser_actionpage_action[参考文档]

  • browser_action :如果你想让图标一直可见,那么配置该项
  • page_action :如果你不想让图标一直可见,那么配置该项

为了让图标一直可见,我们来修改下 mainfest.json

  1. {
  2. ...
  3. "browser_action": {
  4. "default_icon": "icon.png",
  5. "default_title": "McChen"
  6. },
  7. ...
  8. }

此时再次更新查看效果:

到这里,基础的配置开发已经完成了,接下来就是功能部分。

Ⅲ.内容菜单导航开发

[参考文档]

内容导航菜单我用在两个地方:鼠标点击右上角图标的 Popup 和网页中按鼠标右键出现的菜单。

先看看鼠标点击右上角图标 Popup 的,给 mainfest.json 增加 default_popup 就是 popup 展示的页面内容了。

  1. {
  2. ...
  3. "browser_action": {
  4. "default_icon": "icon.png",
  5. "default_title": "McChen",
  6. "default_popup": "popup.html"
  7. },
  8. ...
  9. }

新建一个 popup.html 文件,内容如下:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <title>McChen</title>
  5. <meta charset="utf-8"/>
  6. <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  7. <style type="text/css">
  8. #McChen-container { padding: 4px 0; margin: 0; width: 80px; user-select: none; overflow: hidden; text-align: center; background-color: #f6f8fc;}
  9. .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;}
  10. .McChen-item_a:before { position: absolute; top: 50%; margin-top: -14px; font-size: 16px; line-height: 28px;}
  11. .McChen-item_a:after { position: absolute; top: 50%; margin-top: -14px; font-size: 16px; line-height: 28px;}
  12. .McChen-item_a + .McChen-item_a { border-top: 1px solid #f0f2f5;}
  13. .McChen-item_a:hover { color: #0074ff;}
  14. .McChen-item_a:nth-child(1):before { content: '·'; left: 4px;}
  15. .McChen-item_a:nth-child(2):before { content: '··'; left: 2px;}
  16. .McChen-item_a:nth-child(3):before { content: '···'; left: 0;}
  17. .McChen-item_a:nth-child(4):before { content: '····'; left: -2px;}
  18. .McChen-item_a:nth-child(5):before { content: '····'; margin-top: -16px; left: -2px;}
  19. .McChen-item_a:nth-child(5):after { content: '·'; margin-top: -12px; left: -2px;}
  20. .McChen-item_a:nth-child(6):before { content: '····'; margin-top: -16px; left: -2px;}
  21. .McChen-item_a:nth-child(6):after { content: '··'; margin-top: -12px; left: -2px;}
  22. .McChen-item_a:nth-child(7):before { content: '····'; margin-top: -16px; left: -2px;}
  23. .McChen-item_a:nth-child(7):after { content: '···'; margin-top: -12px; left: -2px;}
  24. </style>
  25. </head>
  26. <body id="McChen-container">
  27. <a class="McChen-item_a" href="https://chenjiahao.xyz" target="_blank">主页</a>
  28. <a class="McChen-item_a" href="https://chenjiahao.xyz/blog/#/archives" target="_blank">博客</a>
  29. <a class="McChen-item_a" href="https://chenjiahao.xyz/blog/#/labels" target="_blank">标签</a>
  30. <a class="McChen-item_a" href="https://chenjiahao.xyz/blog/#/links" target="_blank">友链</a>
  31. <a class="McChen-item_a" href="https://chenjiahao.xyz/blog/#/about" target="_blank">关于</a>
  32. <a class="McChen-item_a" href="https://chenjiahao.xyz/blog/#/board" target="_blank">留言</a>
  33. <a class="McChen-item_a" href="https://chenjiahao.xyz/blog/#/search" target="_blank">搜索</a>
  34. </body>
  35. </html>

我们更新后来看看效果,点击右上角图标将会看到如下的内容弹窗:

下一步,我们来实现在网页中按鼠标右键出现的菜单。

首先,你必须要配置对应的权限才能使用这个 API ,还需要配置修改 mainfest.json 内容:

[权限参考文档]

  1. ...
  2. "permissions": [
  3. "contextMenus"
  4. ]
  5. ...

接下来,需要通过 API 调用去创建对应的菜单,这里需要用到常驻在后台运行的 js 才行,所以还需要修改 mainfest.json 文件:

  1. ...
  2. "background": {
  3. "scripts": [
  4. "background.js"
  5. ]
  6. },
  7. ...

然后我们新建一个 backgroud.js 文件,文件内容如下:

[参考文档]

  1. chrome.contextMenus.create({
  2. id: 'McChen',
  3. title: 'McChen',
  4. contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
  5. });
  6. chrome.contextMenus.create({
  7. id: 'home',
  8. title: '主页',
  9. parentId: 'McChen', // 右键菜单项的父菜单项ID。指定父菜单项将会使此菜单项成为父菜单项的子菜单
  10. contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
  11. });
  12. chrome.contextMenus.create({
  13. id: 'archives',
  14. title: '博客',
  15. parentId: 'McChen', // 右键菜单项的父菜单项ID。指定父菜单项将会使此菜单项成为父菜单项的子菜单
  16. contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
  17. });
  18. chrome.contextMenus.create({
  19. id: 'labels',
  20. title: '标签',
  21. parentId: 'McChen', // 右键菜单项的父菜单项ID。指定父菜单项将会使此菜单项成为父菜单项的子菜单
  22. contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
  23. });
  24. chrome.contextMenus.create({
  25. id: 'links',
  26. title: '友链',
  27. parentId: 'McChen', // 右键菜单项的父菜单项ID。指定父菜单项将会使此菜单项成为父菜单项的子菜单
  28. contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
  29. });
  30. chrome.contextMenus.create({
  31. id: 'about',
  32. title: '关于',
  33. parentId: 'McChen', // 右键菜单项的父菜单项ID。指定父菜单项将会使此菜单项成为父菜单项的子菜单
  34. contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
  35. });
  36. chrome.contextMenus.create({
  37. id: 'board',
  38. title: '留言',
  39. parentId: 'McChen', // 右键菜单项的父菜单项ID。指定父菜单项将会使此菜单项成为父菜单项的子菜单
  40. contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
  41. });
  42. chrome.contextMenus.create({
  43. id: 'search',
  44. title: '搜索',
  45. parentId: 'McChen', // 右键菜单项的父菜单项ID。指定父菜单项将会使此菜单项成为父菜单项的子菜单
  46. contexts: ['page', 'frame', 'selection', 'link', 'editable', 'image', 'video', 'audio', 'page_action']
  47. });
  48. // 监听菜单点击事件
  49. chrome.contextMenus.onClicked.addListener(function (info, tab) {
  50. if (info.menuItemId === 'home') {
  51. chrome.tabs.create({url: 'https://chenjiahao.xyz'});
  52. } else {
  53. chrome.tabs.create({url: 'https://chenjiahao.xyz/blog/#/' + info.menuItemId});
  54. }
  55. })

更新后,点击鼠标右键将查看到如下内容:

至此,内容菜单导航功能已全部完成。

Ⅳ.地址栏搜索开发

[参考文档]

地址栏搜索主要是通过 Omnibox 来实现的,我们首先需要设置关键字,在这里我设置成 'mc' ,修改 mainfest.json 文件:

  1. ...
  2. {
  3. "omnibox": { "keyword" : "mc" }
  4. }
  5. ...

更新后,我们在地址栏输入 mcTab 或者 Space 键可看到如下内容:

接下来我们进行接口开发,由于需要进行接口调用,所以需要配置允许请求的地址,修改 mainfest.json 文件:

  1. ...
  2. {
  3. "permissions": [
  4. "contextMenus",
  5. // 允许请求全部https
  6. "https://*/"
  7. ],
  8. }
  9. ...

然后修改 background.js 文件内容:

  1. ...
  2. let timer = '';
  3. chrome.omnibox.onInputChanged.addListener((text, suggest) => {
  4. if (timer) {
  5. clearTimeout(timer)
  6. timer = ''
  7. } else {
  8. timer = setTimeout(() => {
  9. if (text.length > 1) {
  10. const xhr = new XMLHttpRequest();
  11. xhr.open("POST", "https://api.artfe.club/transfer/github", true);
  12. xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
  13. xhr.onreadystatechange = function () {
  14. if (xhr.readyState === 4) {
  15. const list = JSON.parse(xhr.responseText).data.search.nodes;
  16. if (list.length) {
  17. suggest(list.map(_ => ({content: 'ISSUE_NUMBER:' + _.number, description: '文章 - ' + _.title})))
  18. } else {
  19. suggest([
  20. {content: 'none', description: '无相关结果'}
  21. ])
  22. }
  23. }
  24. };
  25. xhr.send('query=' + query);
  26. } else {
  27. suggest([
  28. {content: 'none', description: '查询中,请稍后...'}
  29. ])
  30. }
  31. }, 300)
  32. }
  33. });
  34. // 当选中建议内容时触发
  35. chrome.omnibox.onInputEntered.addListener((text) => {
  36. if (text.startsWith('ISSUE_NUMBER:')) {
  37. const number = text.substr(13)
  38. chrome.tabs.query({active: true, currentWindow: true}, function (tabs) {
  39. if (tabs.length) {
  40. const tabId = tabs[0].id;
  41. const url = 'https://chenjiahao.xyz/blog/#/archives/' + number;
  42. chrome.tabs.update(tabId, {url: url});
  43. }
  44. });
  45. }
  46. });
  47. ...

这里有几个地方需要注意一下:

  1. onInputChanged 这方法触发频率高,和正常开发一样,需要做一次函数防抖,要不然请求频率会特别高。
  2. 这里面不允许写 Promise ,所以我使用的 XMLHttpRequest
  3. suggestcontentdescription 字段都不允许为空,但是在事件回调里需要识别,所以我这里特意增加了一个前缀 ISSUE_NUMBER:

更新后,在地址栏输入 mcTab 后,输入 干货 ,将会看到如下内容:

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

Ⅴ.新文章推送开发

[存储参考文档]

[推送参考文档]

新文章推送功能,首先我们需要知道之前的最新文章是哪篇,才能做到精准推送,所以这里需要用到 Storage ,也就是存储功能。存下最新文章的 ID ,轮询最新文章,如果有更新,则存下最新文章的 ID 并且调用推送的 API 。所以,我们需要先增加权限配置,修改 mainfest.json 文件:

  1. ...
  2. "permissions": [
  3. "storage",
  4. "contextMenus",
  5. "notifications",
  6. "https://*/"
  7. ],
  8. ...

然后修改 'background.js' 文件内容:

  1. ...
  2. getLatestNumber();
  3. chrome.storage.sync.get({LATEST_TIMER: 0}, function (items) {
  4. if (items.LATEST_TIMER) {
  5. clearInterval(items.LATEST_TIMER)
  6. }
  7. const LATEST_TIMER = setInterval(() => {
  8. getLatestNumber()
  9. }, 1000 * 60 * 60 *24)
  10. chrome.storage.sync.set({LATEST_TIMER: LATEST_TIMER})
  11. });
  12. function getLatestNumber () {
  13. const query = `query {
  14. repository(owner: "ChenJiaH", name: "blog") {
  15. issues(orderBy: {field: CREATED_AT, direction: DESC}, labels: null, first: 1, after: null) {
  16. nodes {
  17. title
  18. number
  19. }
  20. }
  21. }
  22. }`;
  23. const xhr = new XMLHttpRequest();
  24. xhr.open("POST", "https://api.artfe.club/transfer/github", true);
  25. xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  26. xhr.onreadystatechange = function () {
  27. if (xhr.readyState === 4) {
  28. const list = JSON.parse(xhr.responseText).data.repository.issues.nodes;
  29. if (list.length) {
  30. const title = list[0].title;
  31. const ISSUE_NUMBER = list[0].number;
  32. chrome.storage.sync.get({ISSUE_NUMBER: 0}, function(items) {
  33. if (items.ISSUE_NUMBER !== ISSUE_NUMBER) {
  34. chrome.storage.sync.set({ISSUE_NUMBER: ISSUE_NUMBER}, function() {
  35. chrome.notifications.create('McChen', {
  36. type: 'basic',
  37. iconUrl: 'icon.png',
  38. title: '新文章发布通知',
  39. message: title
  40. });
  41. chrome.notifications.onClicked.addListener(function (notificationId) {
  42. if (notificationId === 'McChen') {
  43. chrome.tabs.create({url: 'https://chenjiahao.xyz/blog/#/archives/' + ISSUE_NUMBER});
  44. }
  45. })
  46. });
  47. }
  48. });
  49. }
  50. }
  51. };
  52. xhr.send('query=' + query);
  53. }
  54. ...

注意:由于是后台常驻,所以需要增加轮询来判断是否有更新,我这里设置的是一天一次

更新后,第一次我们会看到浏览器右下角会有推送消息如下:

至此,新文章推送功能也已经开发完成了。

打包发布

在拓展程序页面点击打包扩展程序,选择 src 作为根目录打包即可。

将会生成 src.crxsrc.pem 两个文件, .crx 文件就是你提交到拓展商店的资源, .pem 文件是私钥,下次进行打包更新时需要使用。

由于打包需要 5$ ,所以我这里就不做演示了,需要的可以自行尝试,[发布地址]

结尾

一个基于动态博客的 Chrome 拓展插件 就开发完了,欢迎下载使用。

如有疑问或不对之处,欢迎留言。

(完)


本文为原创文章,可能会更新知识点及修正错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验

如果能给您带去些许帮助,欢迎 ⭐️star 或 ✏️ fork

(转载请注明出处:https://chenjiahao.xyz)

【包教包会】Chrome拓展开发实践的更多相关文章

  1. chrome拓展开发实战:页面脚本的拦截注入

    原文请访问个人博客:chrome拓展开发实战:页面脚本的拦截注入 目前公司产品的无线站点已经实现了业务平台组件化,所有业务组件的转场都是通过路由来完成,而各个模块是通过requirejs进行统一管理, ...

  2. chrome拓展开发实战

    chrome拓展开发实战:页面脚本的拦截注入 时间 2015-07-24 11:15:00  博客园精华区 原文  http://www.cnblogs.com/horve/p/4672890.htm ...

  3. 一个Chrome拓展——HttpPost

    周末花了点时间做了一个chrome拓展,叫HttpPost,顾名思义是用来测试http的post请求. 先直接看效果 插件与拓展 在说这个做的过程前,先说明什么是Chrome插件.Chrome拓展 1 ...

  4. Chrome扩展开发(Gmail附件管理助手)系列之〇——概述

    目录: 0.Chrome扩展开发(Gmail附件管理助手)系列之〇——概述 1.Chrome扩展开发之一——Chrome扩展的文件结构 2.Chrome扩展开发之二——Chrome扩展中脚本的运行机制 ...

  5. 《JavaScript设计模式与开发实践》整理

    最近在研读一本书<JavaScript设计模式与开发实践>,进阶用的. 一.高阶函数 高阶函数是指至少满足下列条件之一的函数. 1. 函数可以作为参数被传递. 2. 函数可以作为返回值输出 ...

  6. Android游戏开发实践(1)之NDK与JNI开发03

    Android游戏开发实践(1)之NDK与JNI开发03 前面已经分享了两篇有关Android平台NDK与JNI开发相关的内容.以下列举前面两篇的链接地址,感兴趣的可以再回顾下.那么,这篇继续这个小专 ...

  7. TFS 2015 敏捷开发实践 – 在Kanban上运行一个Sprint

    前言:在 上一篇 TFS2015敏捷开发实践 中,我们给大家介绍了TFS2015中看板的基本使用和功能,这一篇中我们来看一个具体的场景,如何使用看板来运行一个sprint.Sprint是Scrum对迭 ...

  8. Android游戏开发实践(1)之NDK与JNI开发01

    Android游戏开发实践(1)之NDK与JNI开发01 NDK是Native Developement Kit的缩写,顾名思义,NDK是Google提供的一套原生Java代码与本地C/C++代码&q ...

  9. Android游戏开发实践(1)之NDK与JNI开发02

    Android游戏开发实践(1)之NDK与JNI开发02 承接上篇Android游戏开发实践(1)之NDK与JNI开发01分享完JNI的基础和简要开发流程之后,再来分享下在Android环境下的JNI ...

随机推荐

  1. GA,RC,Alpha,Beta,Final等软件版本名词释义

    对应上图的表格如下: 名词 说明 Alpha α是希腊字母的第一个,表示最早的版本,内部测试版,一般不向外部发布,bug会比较多,功能也不全,一般只有测试人员使用. Beta β是希腊字母的第二个,公 ...

  2. 从原理到场景 系统讲解 PHP 缓存技术

    第1章课程介绍 此为PHP相关缓存技术的课堂,有哪些主流的缓存技术可以被使用? 第1章 课程介绍 1-1课程介绍1-2布置缓存的目的1-3合理使用缓存1-4哪些环节适合用缓存 第2章 文件类缓存 2- ...

  3. CentOS7 安装 Pure-ftpd

    博客地址:http://www.moonxy.com 一.摘要 FTP 是 File Transfer Protocol(文件传输协议)的英文简称,而中文简称为"文传协议”.用于Intern ...

  4. Day 11 文件的权限

    1.什么是权限? 我们可以把它理解为操作系统对用户能够执行的功能所设立的限制,主要用于约束用户能对系统所做的操作,以及内容访问的范围,或者说,权限是指某个特定的用户具有特定的系统资源使用权力.* 2. ...

  5. 30 (OC)* 数据结构和算法

    在描述算法时通常用o(1), o(n), o(logn), o(nlogn) 来说明时间复杂度 o(1):是最低的时空复杂度,也就是耗时/耗空间与输入数据大小无关,无论输入数据增大多少倍,耗时/耗空间 ...

  6. 07-SQLServer数据库中的系统数据库

    一.总结 首先要明确SQLServer的系统数据库一共有5个:Master.Model.Msdb.Tempdb.Resource. 1.Master数据库 (1)master数据库记录了所有系统级别的 ...

  7. 让API实现版本管理的实践

    API版本管理的重要性不言而喻,对于API的设计者和使用者而言,版本管理都有着非常重要的意义.下面会从WEB API 版本管理的角度提供几种常见办法: 首先,对于API的设计和实现者而言,需要考虑向后 ...

  8. [AWS] 02 - Pipeline on EMR

    Data Analysis with EMR. Video demo: Run Spark Application(Scala) on Amazon EMR (Elastic MapReduce) c ...

  9. Winform组合ComboBox和TreeView实现ComboTree

    最近做Winform项目需要用到类似ComboBox的TreeView控件. 虽然各种第三方控件很多,但是存在各种版本不兼容问题.所以自己写了个简单的ComboTreeView控件. 下图是实现效果: ...

  10. asp.net core mvc 之 DynamicApi

    这段时间闲赋在家,感觉手痒,故想折腾一些东西. 由于之前移植了一个c#版本的spring cloud feign客户端(https://github.com/daixinkai/feign.net), ...