实战写浏览器插件

写一些浏览器脚本或者插件方便平时的自动化操作.
油猴脚本编写不难,但考虑到浏览器插件可能涉及功能多同时并不是所有人会去安装一个类似油猴的插件,所以这里选择开发插件.

现在Edge浏览器和Chrome浏览器都差不太多,需要修改一些API即可进行移植,这里选择Chrome浏览器,因为谷歌官方教程写得更详细一些,这里使用的是最新的V3版本.

如果要从谷歌浏览器插件迁移到Edge,也只需要修改一些不支持的API即可

Microsoft Edge 不支持以下扩展 API:

  • chrome.gcm.
  • chrome.identity.getAccounts.
  • chrome.identity.getAuthToken - 作为替代项,可以使用 launchWebAuthFlow 提取 OAuth2 令牌来对用户进行身份验证。
  • chrome.instanceID.

本地开发时,直接本地导入然后编辑文件能同步的.

文件结构

The manifest
扩展的manifest是唯一必需的文件,它必须有一个特定的文件名:manifest.json。它还必须位于扩展的根目录中。清单记录了重要的元数据、定义了资源、声明了权限,并确定了要在后台和页面上运行的文件。
The service worker
扩展服务程序负责处理和监听浏览器事件。事件有很多种,例如导航到新页面、删除书签或关闭标签页。它可以使用所有 Chrome API,但不能直接与网页内容交互;这是内容脚本的工作。
content script
内容脚本在网页上下文中执行 Javascript。它们还可以读取和修改注入页面的 DOM。内容脚本只能使用 Chrome API 的一个子集,但可以通过与扩展服务 Worker 交换信息来间接访问其余 API。
The popup and other pages
扩展可以包含各种 HTML 文件,如弹出窗口、选项页面和其他 HTML 页面。所有这些页面都可以访问 Chrome API。

常用文件结构

The contents of an extension folder: manifest.json, background.js, scripts folder, popup folder, and images folder.

Manifest.json

清单文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"manifest_version": 3,
"name": "HightLight",
"version": "1.0",

"action": {
"default_popup": "index.html",
"default_icon": "img/icon.png"
},
"default_locale": "en",
"description": "highlight what you select",
"icons": {
"16": "img/icon.png",
"48": "img/icon.png",
"128": "img/icon.png"
}
}

其中,action键指定 Chrome 浏览器应作为扩展的操作图标使用的图片,以及在点击扩展的操作图标时弹出的 HTML 页面.icons里的选项对应不同的展示.

Icon sizeIcon use
16x16Favicon on the extension’s pages and context menu icon.
32x32Windows computers often require this size.
48x48Displays on the Extensions page.
128x128Displays on installation and in the Chrome Web Store.

功能性文件如下,常使用conten-script,popopjs以及background.js

img

其中injected script其实就是使用content-script向DOM中插入js.

devtools.js在vue以及react的开发者工具中使用到.

Popup文件

1
2
3
4
5
6
{
"action": {
"default_popup": "popup/index.html",
"default_icon": "img/icon.png"
},
}

由于单击图标打开popup,焦点离开又立即关闭,所以popup页面的生命周期一般很短,需要长时间运行的代码不要写在popup里面。

content-script

content-script设置扩展可以运行读取和修改页面内容的脚本。

这些脚本被称为内容脚本。它们是孤立的,这意味着它们可以更改自己的 JavaScript 环境,而不会与其主机页面或其他扩展的内容脚本发生冲突。

1
2
3
4
5
6
7
8
9
10
11
12
{
...
"content_scripts": [
{
"js": ["scripts/content.js"],
"matches": [
"https://developer.chrome.com/docs/extensions/*",
"https://developer.chrome.com/docs/webstore/*"
]
}
]
}

内容脚本可以使用标准文档对象模型(DOM)来读取和更改页面内容.

此外还可以注入css以及设置代码注入时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
// 需要直接注入页面的JS
"content_scripts":
[
{
//"matches": ["http://*/*", "https://*/*"],
// "<all_urls>" 表示匹配所有地址
"matches": ["<all_urls>"],
// 多个JS按顺序注入
"js": ["js/jquery-1.8.3.js", "js/content-script.js"],
// JS的注入可以随便一点,但是CSS的注意就要千万小心了,因为一不小心就可能影响全局样式
"css": ["css/custom.css"],
// 代码注入的时间,可选值: "document_start", "document_end", or "document_idle",最后一个表示页面空闲时,默认document_idle
"run_at": "document_start"
}
],
}

content-scripts不能访问绝大部分chrome.xxx.api,除了下面这4种:

  • chrome.extension(getURL , inIncognitoContext , lastError , onRequest , sendRequest)
  • chrome.i18n
  • chrome.runtime(connect , getManifest , getURL , id , onConnect , onMessage , sendMessage)
  • chrome.storage

content-scripts和原始页面共享DOM,但是不共享JS,如要访问页面JS(例如某个JS变量),只能通过injected js来实现

background

  • 不使用时终止,需要时重新启动(类似于事件页面)。
  • 无权访问 DOM。(service worker独立于页面)

执行js代码以及css

可以注册事件hrome.action.onClicked,但是这与action的default_popup冲突,只能使用其中一个.

image-20230809232026961

此外,manifest.json中有permissions以及host_permissions用于权限申请

1
2
3
4
5
6
{
"permissions": ["activeTab", "storage", "scripting"],
"host_permissions": [
"https://developer.chrome.com/*"
],
}

activeTab 权限允许用户有目的地选择在重点标签页上运行扩展;这样可以保护用户的隐私。另一个好处是它不会触发权限警告。

主机权限允许我们向特定网站授予更高的权限,从而保护用户隐私。这将授予对标题和 URL 属性以及其他功能的访问权限。

JS种类可访问的APIDOM访问情况JS访问情况直接跨域
injected script和普通JS无任何差别,不能访问任何扩展API可以访问可以访问不可以
content script只能访问 extension、runtime等部分API可以访问不可以不可以
popup js可访问绝大部分API,除了devtools系列不可直接访问不可以可以
background js可访问绝大部分API,除了devtools系列不可直接访问不可以可以
devtools js只能访问 devtools、extension、runtime等部分API可以访问devtools可以访问devtools不可以

注册快捷键

1
2
3
4
5
6
7
8
9
10
{
"commands": {
"_execute_action": {
"suggested_key": {
"default": "Ctrl+B",
"mac": "Command+B"
}
}
}
}

这里的_execute_action相当于服务活动进程里的action.onClicked

通信机制

img

经常使用的通信包括popup向content-script发送,因为前者不能直接访问DOM,而后者用户不能直接与其交互,同时popup与background.js相互交互也很常用.

对于popup与background之间通信,可以使用chrome.extension.getBackgroundPagechrome.extension.getViews.而其他之间的通信一般chrome.runtime.onMessage以及对应的sendMessage还有popup的chrome.tabs.sendMessage,此外还可以使用socket实现长通信chrome.tabs.connectchrome.runtime.connect.

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
// background.js
function test()
{
alert('我是background!');
}

// popup.js
var bg = chrome.extension.getBackgroundPage();
bg.test(); // 访问bg的函数
// background.js
function test()
{
alert('我是background!');
}
--------------------------
var views = chrome.extension.getViews({type:'popup'});
if(views.length > 0) {
console.log(views[0].location.href);
}
--------------------------
function sendMessageToContentScript(message, callback)
{
chrome.tabs.query({active: true, currentWindow: true}, function(tabs)
{
chrome.tabs.sendMessage(tabs[0].id, message, function(response)
{
if(callback) callback(response);
});
});
}
sendMessageToContentScript({cmd:'test', value:'你好,我是popup!'}, function(response)
{
console.log('来自content的回复:'+response);
});
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse)
{
// console.log(sender.tab ?"from a content script:" + sender.tab.url :"from the extension");
if(request.cmd == 'test') alert(request.value);
sendResponse('我收到了你的消息!');
});
-----------------------------------
chrome.runtime.sendMessage({greeting: '你好,我是content-script呀,我主动发消息给后台!'}, function(response) {
console.log('收到来自后台的回复:' + response);
});
// 监听来自content-script的消息
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse)
{
console.log('收到来自content-script的消息:');
console.log(request, sender, sendResponse);
sendResponse('我是后台,我已收到你的消息:' + JSON.stringify(request));
});

发布程序

如果只需要自己本地把插件放在浏览器上使用,就不需要发布了.如果想要发布的话,需要注册谷歌开发者账号Chrome 应用商店 - 开发者协议 (google.com),需要5美元.事实上如果你只需要把插件给周围的人用的话,就不需要发布到插件商城了.

本地的话,edge://extensions/页面可以加载解压缩的扩展或者打包扩展.

image-20230809152013037

插件根目录新建一个名为_locales的文件夹,再在下面新建一些语言的文件夹,如enzh_CNzh_TW,然后再在每个文件夹放入一个messages.json,同时必须在清单文件中设置default_locale

_locales\en\messages.json内容:

1
2
3
4
{
"pluginDesc": {"message": "A simple chrome extension demo"},
"helloWorld": {"message": "Hello World!"}
}

_locales\zh_CN\messages.json内容:

1
2
3
4
{
"pluginDesc": {"message": "一个简单的Chrome插件demo"},
"helloWorld": {"message": "你好啊,世界!"}
}

manifest.json中设置默认语言

1
2
3
{
"default_locale": "zh_CN",
}

最后我写了一个简单的划重点的扩展,用户点击扩展后可以自己选择某种预定的颜色或者自定义某种颜色,然后光标选中的文字就会根据选择变色作为画出的重点.

image-20230810001157344

image-20230810001532236

遇到的一些问题

  1. 扩展图标default_icon对png格式支持比较好,其他格式官网说支持,但我感觉支持得并不好.
  2. 改动pop.html以及其引入的css,js文件不需要重新加载,其他的文件改动需要重新加载.

参考资料

  1. 创建扩展教程,第 1 部分 - Microsoft Edge Development | Microsoft Learn
  2. Chrome Extensions 101 - Chrome Developers
  3. 我做了个插件,让你瞬间了解你B站上出现的每一个UP主_哔哩哔哩_bilibili
  4. Chrome插件开发简要指南_w3cschool
  5. chrome-extension/chrome 插件开发.md at main · caifeng123/chrome-extension (github.com)
-------------本文结束感谢您的阅读-------------
感谢阅读.

欢迎关注我的其它发布渠道