前言
在平时的工作机中,一般安装了两个浏览器,一个最新版的 Chrome
专用于前端开发,一个 360极速浏览器
用于日常事务处理。但由于某些原因,工作机不再允许安装 360浏览器
,但是 Chrome浏览器
中缺少了一个非常便捷的功能:工具栏和右键中的自定义引擎搜索
,缺少了这个功能非常难受,感觉搜索的效率直线下降。
Chrome
扩展市场中倒是有一些类似功能的插件,但是用起来总是不如意。恰逢疫情突然变重,过年没办法回老家,窝在武汉手写一个浏览器扩展 Rummage
来实现这些功能。
最近刚刚谷歌扩展市场审核通过,所以可以直接安装:Rummage
,访问不了的话可以下载 crx 拖到浏览器中:magee.lanzous.com/iVZ89moyubg 密码:2021,总结下开发的过程。
产品设计
分析下需求,核心需求主要是三个:
- 可自定义配置搜索引擎
- 在工具栏中可以输入内容并选择引擎后搜索
- 页面中选中文本后,右键菜单中选择引擎搜索指定文本
锦上添花的需求如下:
- 自定义配置搜索引擎时,可多配置一些功能:新页面打开,无痕模式打开,默认搜索引擎可以被动态修改
- 显示搜索引擎的
Favicon
,便于识别 - 工具栏中能保存并回显搜索记录
- 导入导出配置
- 实现国际化
调研
需求分析完了,但是现在有一个问题,我从来没开发过浏览器扩展,两眼一抹黑。搜了很多资料并看了官方文档后总结如下:
扩展文档
如果英文水平不错,可以直接官网文档:developer.chrome.com/docs/extens…。
另外,有大佬之前详细的总结过的一篇博客——【干货】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
的配置:
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
配置:
{
// ...
"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
格式。
具体的代码写的有点乱,如下所示:
/**
* @description: 获取指定url的favicon链接
* @param {String} url
* @return {String} url
*/
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;
}
};
/**
* @description: 校验url
* @param {string} string
* @return {*}
*/
const isValidHttpOrHttpsUrl = (string) => {
let url;
try {
url = new URL(string);
} catch (_) {
return false;
}
return url.protocol === 'http:' || url.protocol === 'https:';
};
/**
* @description: 从 DOM 的 head 中 获取 link 指向的url
* @param {String} url
* @return {String} url
*/
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;
}
};
/**
* @description: 规范化 favicon 的 URL 链接
* @param {String} rawHref
* @param {String} url
* @return {String} 返回 favicon 的完整链接
*/
const formatHref = (rawHref, url) => {
try {
let urlObj = new URL(url);
if (rawHref == null) {
return `${urlObj.origin}/favicon.ico`;
}
// start with http or https
if (rawHref.startsWith('http')) {
return rawHref;
}
// start with //
if (rawHref.startsWith('//')) {
return `${urlObj.protocol}${rawHref}`;
}
// start with /
if (rawHref.startsWith('/')) {
return `${urlObj.origin}${rawHref}`;
}
// default, root path + /favicon.ico
} catch (_) {
return null;
}
};
/**
* @description: 将 IMG元素 转为 base64
* @param {IMGElement} imgElement
* @param {Number} width 宽
* @param {Number} height 高
* @return {String} base64
*/
const getBase64Image = (imgElement, width, height) => {
try {
//width、height调用时传入具体像素值,控制大小 ,不传则默认图像大小
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;
}
};
/**
* @description: 将 图片URL 转为 base64
* @param {String} imgUrl
* @return {String} base64
*/
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;
}
};
/**
* @description: 传入链接,返回对应网站的base64
* @param {String} url
* @return {String} base64
*/
const getBase64FromUrl = async (url) => {
// debugger;
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
导出:
// func.js
const funcA = () => {
console.log('funcA函数执行');
};
const funcB = () => {
console.log('funcB函数执行');
};
export { funcA, funcB };
background.js
导入:
// background.js
(async () => {
const funcURL = chrome.runtime.getURL('js/func.js');
const funcMain = await import(funcURL);
funcMain.funcA(); // output: funcA函数执行
funcMain.funcB(); // output: 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
:
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
也是类似的步骤。
前端记事本,不定期更新,欢迎关注!
- 微信公众号: 林景宜的记事本
- 博客:林景宜的记事本
- 掘金专栏:林景宜的记事本
- 知乎专栏: 林景宜的记事本
- Github: MageeLin
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!