
前言
在平时的工作机中,一般安装了两个浏览器,一个最新版的 Chrome
专用于前端开发,一个 360极速浏览器
用于日常事务处理。但由于某些原因,工作机不再允许安装 360浏览器
,但是 Chrome浏览器
中缺少了一个非常便捷的功能:工具栏和右键中的自定义引擎搜索
,缺少了这个功能非常难受,感觉搜索的效率直线下降。
Chrome
扩展市场中倒是有一些类似功能的插件,但是用起来总是不如意。恰逢疫情突然变重,过年没办法回老家,窝在武汉手写一个浏览器扩展 Rummage
来实现这些功能。
最近刚刚谷歌扩展市场审核通过,所以可以直接安装:Rummage
,访问不了的话可以下载 crx 拖到浏览器中:https://magee.lanzous.com/iVZ89moyubg 密码:2021,总结下开发的过程。
产品设计
分析下需求,核心需求主要是三个:
- 可自定义配置搜索引擎
- 在工具栏中可以输入内容并选择引擎后搜索
- 页面中选中文本后,右键菜单中选择引擎搜索指定文本
锦上添花的需求如下:
- 自定义配置搜索引擎时,可多配置一些功能:新页面打开,无痕模式打开,默认搜索引擎可以被动态修改
- 显示搜索引擎的
Favicon
,便于识别
- 工具栏中能保存并回显搜索记录
- 导入导出配置
- 实现国际化
调研
需求分析完了,但是现在有一个问题,我从来没开发过浏览器扩展,两眼一抹黑。搜了很多资料并看了官方文档后总结如下:
扩展文档
如果英文水平不错,可以直接官网文档:https://developer.chrome.com/docs/extensions/mv3/。
另外,有大佬之前详细的总结过的一篇博客——【干货】Chrome 插件(扩展)开发全攻略。
UI 框架
大致学会了扩展的开发流程,核心就是 manifest.json
,通过这个配置文件指向需要的文件,另外还有很多专用的 API
用于和浏览器交互。
所以在开发中,只要能在打包时生成对应文件,就可以开发和打包分开,也就可以引入体积较大的 UI框架
来敏捷开发。但是另一个问题来了,我对 Webpack
玩的不是很溜,自己配置一个太费功夫。
功夫不负有心人,多番搜索发现了 Vue
的一个小众 UI框架
—— Quasar
,这个框架不仅样式精美,它的 CLI 中自带一个浏览器扩展开发模式 Quasar Bex
,可以自动生成一个与 src
层级并列的 src-bex
文件夹,不管是 manifest.json
还是其他文件都已经配置妥当,只需要按照自己的需要来开发就可以了。

开发
首先给插件起个名叫“翻查
”,英文名 “Rummage
”,其次扩展还需要个图标,在 Iconfont
上搜了一个挺好看的图标。开发过程中其实只需要三个部分:
- 点击扩展图标后的
Popup
弹出页(需先配置 manifest.json
中的 browser_action.default_popup
)
- 内置页面:右键选扩展图标后点击选项,新打开的扩展配置和说明页面(需先配置
manifest.json
中的 options_page
)
- 页面中点击右键后的菜单配置(需先配置
manifest.json
中的 background
和 permissions
)
Quasar
的 bex
模式已经配置好,只需要将路由与 manifest.json
文件匹配即可。
下面是 vue-router
的配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const routes = [ { path: '/', component: () => import('layouts/MainLayout.vue'), children: [ { path: '', component: () => import('pages/Index.vue') }, { path: 'options', component: () => import('pages/Options.vue') }, { path: 'about', component: () => import('pages/about.vue') }, ], }, { path: '/popup', component: () => import('pages/Popup.vue'), children: [], }, { path: '*', component: () => import('pages/Error404.vue'), }, ];
export default routes;
|
对应的 manifest.json
配置:
1 2 3 4 5 6 7 8 9
| { "options_page": "www/index.html#/options", "browser_action": { "default_title": "__MSG_ext_title__", "default_popup": "www/index.html#/popup" } }
|
浏览器端保存 Favicon
前两种页面的开发就是很普通的 Vue
页面的开发方式,唯一多费了脑筋的地方是保存搜索引擎的 favicon
上。
最后的实现步骤是如下方式:
- 先请求给定的链接,再用浏览器自带的
DOMParser API
来解析 DOM
。
- 判断页面的
<head />
中是否有 rel
属性为 "shortcut icon"
或 "icon"
的<link/>
标签,假如有则保存 favicon
的 url
。
- 如果没有则将
URL
设为协议:域名/favicon
。
- 统一规范化
URL
。
- 用 Image 对象请求
URL
后,使用 Canvas
加载图片元素。
- 将图片导出成为
Base64
格式。
具体的代码写的有点乱,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
|
const getFaviconUrl = async (url) => { if (!isValidHttpOrHttpsUrl(url)) { return null; }
try { url = new URL(url).origin; let href = await getHref(url); let pathFormated = formatHref(href, url); return pathFormated; } catch (_) { return null; } };
const isValidHttpOrHttpsUrl = (string) => { let url; try { url = new URL(string); } catch (_) { return false; }
return url.protocol === 'http:' || url.protocol === 'https:'; };
const getHref = async (url) => { try { let res = await fetch(url); let resText = await res.text(); let resHtml = new DOMParser().parseFromString(resText, 'text/html'); let linkHtml = resHtml.querySelector('link[rel="icon"]') ?? resHtml.querySelector('link[rel="shortcut icon"]'); let href = linkHtml.getAttribute('href'); return href; } catch (_) { return null; } };
const formatHref = (rawHref, url) => { try { let urlObj = new URL(url); if (rawHref == null) { return `${urlObj.origin}/favicon.ico`; } if (rawHref.startsWith('http')) { return rawHref; } if (rawHref.startsWith('//')) { return `${urlObj.protocol}${rawHref}`; } if (rawHref.startsWith('/')) { return `${urlObj.origin}${rawHref}`; }
} catch (_) { return null; } };
const getBase64Image = (imgElement, width, height) => { try { let canvas = document.createElement('canvas'); canvas.width = width ? width : imgElement.width <= 20 ? imgElement.width : 20; canvas.height = height ? height : imgElement.height <= 20 ? imgElement.height : 20;
let ctx = canvas.getContext('2d'); ctx.drawImage(imgElement, 0, 0, canvas.width, canvas.height); let dataURL = canvas.toDataURL('image/gif', 0.8); return dataURL; } catch (_) { return null; } };
const getBase64FromFaviconUrl = async (imgUrl) => { if (imgUrl == null) { return null; } try { let imgElement = new Image(); imgElement.crossOrigin = ''; imgElement.src = imgUrl; let imgPromise = new Promise((resolve, reject) => { if (imgUrl) { imgElement.onload = function () { resolve(getBase64Image(imgElement)); }; imgElement.onerror = function () { reject(); }; } }); return await imgPromise; } catch (_) { return null; } };
const getBase64FromUrl = async (url) => { try { let faviconUrl = await getFaviconUrl(url); let faviconBase64 = await getBase64FromFaviconUrl(faviconUrl); return faviconBase64; } catch (_) { return null; } };
|
background.js 的模块化
最后一种 background.js
的开发主要是为了能够配置右键菜单。都是对照着谷歌官方文档开发就可以,但是有一个地方比较特殊——模块化。
如果想 import
或 export
一些公用方法,普通的 import 'XXXX' from 'XXXX.js';
的模式是不生效的,得用如下的特殊方法:
模块 js
导出:
1 2 3 4 5 6 7 8
| const funcA = () => { console.log('funcA函数执行'); }; const funcB = () => { console.log('funcB函数执行'); }; export { funcA, funcB };
|
background.js
导入:
1 2 3 4 5 6 7 8
| (async () => { const funcURL = chrome.runtime.getURL('js/func.js'); const funcMain = await import(funcURL);
funcMain.funcA(); funcMain.funcB(); })();
|
存储
对于扩展来说,只是单纯的展示页面,可以用普通页面存储用到的 cookies
或 localStorage
。但是如果需要与浏览器交互,比如给右键配置菜单,就需要用扩展专用的负责存储的 API
: chrome.storage.local
和 chrome.storage.sync
。
存储 |
chrome.storage.local |
chrome.storage.sync |
window.localStorage |
总最大限制 |
可无限大 |
100KB |
5MB |
单条最大限制 |
可无限大 |
8KB |
5MB |
修改频率限制 |
可无限大 |
8KB |
1800 次/小时 |
存储格式 |
可直接存储对象 |
可直接存储对象 |
只可存储字符串 |
数据同步方式 |
手动导入导出 |
自动跨设备同步 |
手动导入导出 |
事件 |
支持 |
支持 |
其他页面修改才会触发 |
可用位置 |
可用于 content 和 background |
可用于 content 和 background |
只能用于插件自身页面 |
写了一些工具方法,把的异步存储操作的回调变为了 async
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| const setStorageLocal = async (items) => { let result = await new Promise((resolve) => { chrome.storage.local.set(items, () => { console.log('保存成功', items); resolve(items); }); }); return result; };
const getStorageLocal = async (keys) => { return await new Promise((resolve) => { chrome.storage.local.get(keys, (items) => { console.log('获取成功', items); resolve(items); }); }); };
const removeStorageLocal = async (keys) => { return await new Promise((resolve) => { chrome.storage.local.remove(keys, () => { console.log('删除成功', keys); resolve(keys); }); }); };
const clearStorageLocal = async () => { return await new Promise((resolve) => { chrome.storage.local.clear(() => { console.log('清空成功'); resolve(true); }); }); };
|
发布
Quasar cli
中自带的 bex
打包模式,将插件打包到 dist
中,有 Chrome
版,FireFox
版和未压缩版。

发布到 Google
应用市场需要做一些准备工作,主要是需要以下准备内容:
- 如果没有谷歌
Web
的开发者账户,需要准备可支付 5
美元的信用卡来注册开发者。如果开发移动端 APP
,注册谷歌 Play
开发者需要 25
美元,相比下页面端还是便宜。
- 扩展图标
128x128
像素,png
格式。
- 插件名字,根据你扩展的
i18n
支持的数量准备不同语言下的名字。
- 插件介绍,与上条类似。
- 屏幕截图,
1280x800
或 640x400
JPEG
或 24
位 PNG
(无 alpha
透明层),每种语言不得多于 5
张,不得少于一张。
- 全球通用的屏幕截图:不区分语言,条件与上一条相同。
- 小型宣传图块,不区分语言,
440x280
,JPEG
或 24
位 PNG
(无 alpha
透明层)。
- 大型宣传图块,不区分语言,
920x680
,JPEG
或 24
位 PNG
(无 alpha
透明层)。
- 顶部宣传图块,不区分语言,
1400x560
,JPEG
或 24
位 PNG
(无 alpha
透明层)。
manifest.json
中需要启用的权限,每种权限都需要说明用途。

全都填完后就可以提交审核,大概需要一周的时间。发布到 EDGE
或者 FireFox
也是类似的步骤。
前端记事本,不定期更新,欢迎关注!