Chrome 插件开发流程

初始化项目

  1. 新建项目
  2. 编写一个manifest.json文件来描述你的插件。
1
2
3
4
5
6
7
8
9
10
11
12
{
"manifest_version": 3,
"name": "weirdo-message",
"version": "1.0.0",
"description": "一个图片下载插件",
"icons": {
"16": "icon16.png",
"32": "icon32.png",
"48": "icon48.png",
"128": "icon128.png"
}
}

本地插件的安装需要打开开发模式 在浏览器输入chrome://extensions/打开插件页面,点击右上角的开发者模式,在点击加载已解压的扩展程序,选择我们刚刚新建的那个插件项目文件夹。

添加功能

我们给插件添加第一个功能:鼠标移动到图片元素上,显示图片的信息(存储大小,真实尺寸,显示尺寸)

由于我们的插件是需要操作 dom,并且不需要一直在后台运行,只需要再打开网页的时候运行。

所以我们使用内容脚本 content_scripts 的方式运行插件即可。

内容脚本(content_scripts)的特性:

  • 在页面打开,或者页面加载结束,或者页面空闲的时候注入
  • 共享页面 dom,也就是说可以操作页面的 dom
  • JS是隔离的,插件中的js定义并不会影响页面的js,也不能引用页面中的js变量、函数

content_scripts 有多种使用方式:

  1. 静态注入。在manifest.json文件中声明
  2. 动态注入。chrome.scripting.registerContentScripts
  3. 编码注入。chrome.scripting.executeScript

使用静态注入

manifest.json文件中添加一下content_scripts配置:

1
2
3
4
5
6
7
8
9
10
{
...,
"content_scripts": [
{
"matches": ["https://*/*"],
"js": ["src/main.js"]
}
],
...
}

使用动态注入

通过调用api的方法来注入

1
2
3
4
5
6
7
8
9
chrome.scripting.registerContentScripts([{
id: "session-script",
js: ["content.js"], // 要执行的的js文件
persistAcrossSessions: false,
matches: ["*://example.com/*"], // 被注入的页面
runAt: "document_start",
}])
.then(() => console.log("registration complete"))
.catch((err) => console.warn("unexpected error", err))

动态注入可以 scripts 注入的时机更可控、或者可以更新、删除 content_scripts

content_scripts属性是一个数组,也就是说我们可以配置多个脚本规则,数组的每个元素包含多个属性:

  • matches : 指定此内容脚本将被注入到哪些页面。必填
  • js : 要注入匹配页面的 JavaScript 文件列表。选填
  • css : 要注入匹配页面的 CSS 文件列表。选填
  • run_at : 指定何时应将脚本注入页面。

有三种类型,document_start,document_end,document_idle。默认为document_idle。选填

一般我们就采用静态注入, 比较方便

显示图片基本信息:

添加第一个功能,显示图片基本信息:

src/main.js

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
/**
* 显示网络图片的内存大小
* @param {*} src
* @returns
*/
function getByte(src){
return fetch(src).then(function(res){
return res.blob()
}).then(function(data){
return (data.size/(1024)).toFixed(2)+'kB'
})
}

/**
* 基于dom的title属性来设置显示图片信息
* @param {*} el
* @param {number} byte zijie
*/
function showInfo(el,byte){
var html=`真实尺寸:${el.naturalWidth}*${el.naturalHeight}\n显示尺寸:${el.width}*${el.height}\n存储大小:${byte}`;
el.title=html
}

/**
* 在document上代理mouseover事件
*/
document.addEventListener('mouseover',function(e){
//移动到图片元素上时、则显示信息
if(e.target.tagName=='IMG'){
getByte(e.target.src).then(byte => {
showInfo(e.target, byte)
})
}
}, true) // true为捕获阶段

给图片元素添加拖拽事件

接下来我们给图片元素添加拖拽事件,因为是拖拽下载,所以我们只使用 dragend 事件即可.

1
2
3
4
5
6
7
8
/**
* 在document上代理mouseover事件
*/
document.addEventListener('dragend',function(e){
if(e.target.tagName=='IMG'){
//TODO 下载
}
})

实现下载功能

实现图片下载功能有俩种方式:

  1. 在内容脚本(content_scripts)中使用原生js实现下载图片功能
  2. 使用插件 chrome.downloads.downloadAPI 实现下载

原生实现下载这里就不赘述了

我们使用 chrome.downloads.download 来实现下载。

我们需要在 manifest.json 中添加 downloads 权限来使用该 api

1
2
3
4
5
6
7
{
...,
"permissions": [
"downloads"
],
...
}

注意,chrome 的部分api不能直接在content_scripts中使用,所以我们需要一个后台页面来使用这个api来实现下载。

添加后台页面脚本( background )配置:

1
2
3
4
5
6
7
{
...,
"background":{
"service_worker": "src/service_worker.js"
},
...
}

我们需要一个新建后台脚本src/service_worker.js

1
2
3
4
5
6
7
//service_worker.js
function download(url){
var options = {
url:url
}
chrome.downloads.download(options)
}

然后我们需要利用页面通信机制在content_scripts中调用background中的函数。

由于content_scripts是在网页中运行的,而非在扩展的上下文中,因此它们通常需要某种方式与扩展的其余部分进行通信。

扩展页面(options_page, bakcground, popup)和内容脚本(content_scripts)之间的通信通过使用消息传递进行。

任何一方都可以侦听从另一端发送的消息,并在同一通道上做出响应。

消息可以包含任何有效的 JSON 对象(空值、布尔值、数字、字符串、数组或对象)

从内容脚本(content_scripts) 发送到 扩展页面(options_page,bakcground,popup

1
2
3
4
(async () => {
const response = await chrome.runtime.sendMessage({greeting: "hello"});
console.log(response);
})();

从扩展页面(options_page,bakcground,popup)发送到 内容脚本(content_scripts)

1
2
3
4
5
6
(async () => {
//获取当前的tab页面
const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
const response = await chrome.tabs.sendMessage(tab.id, {greeting: "hello"});
console.log(response);
})();

接收消息的方法都是一样的,通过runtime.onMessage事件侦听器来处理消息

1
2
3
4
5
6
7
8
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
console.log(sender.tab ? "from a content script:" + sender.tab.url : "from the extension");
if (request.greeting === "hello")
//处理完消息后、通知发送方
sendResponse({farewell: "goodbye"});
}
);

除了上面介绍runtime.onMessage的方式进行通信。插件还提供了长连接和消息传递API的方法来实现通信, 具体可访问官方文档

实现下载

在前面的main.js脚本中的dragend事件中,添加发送消息的代码:

1
2
3
4
5
6
7
8
9
/**
* 在document上代理dragend事件
*/
document.addEventListener('dragend', async function(e){
if(e.target.tagName == 'IMG'){
//发生消息,从content_scripts发送到扩展页面
await chrome.runtime.sendMessage({type:'down', data:e.target.src});
}
})

然后在src/service_worker.js中添加接收消息的处理器:

1
2
3
4
5
6
7
8
9
10
11
12
function download(url){
var options = {
url:url
}
chrome.downloads.download(options)
}
//接收消息处理器
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if (message.type == 'down') {
download(message.data) //调用下载方法
}
});

这样就完成了下载功能的开发。

右键菜单批量下载图片

右键菜单功能需要权限配置,在manifest.json中添加权限配置:

1
2
3
4
5
6
7
{
...,
"permissions": [
"contextMenus"
],
...
}

我们需要显示一个菜单前的图标,需要再icons里配置一个16像素的图标。我们在之前已经配置好了。

contextMenusapi也不能在content_scripts中使用。所以需要在src/service_worker.js中创建菜单。

1
2
3
4
5
6
chrome.contextMenus.create({
type: 'normal',
title: '右键菜单',
contexts:['all'],
id:'menu-1'
});
  • type: 用于配置菜单的类型,有4种类型:普通菜单,复选菜单,单选菜单,分割线。
  • title: 菜单的名字。
  • contexts: 用于配置菜单在什么情况下可以显示。

包括all、page、frame、selection、link、editable、image、video、audio和 launcher。比如在有内容被选择的时候才显示菜单

  • id: 菜单的编号,唯一。

同时可以给菜单配置子菜单

1
2
3
4
5
6
7
chrome.contextMenus.create({
type: 'normal',
title: '右键菜单-子',
contexts:['all'],
id:'menu-2',
parentId:'menu-1'
});

对于我们这个插件而言,只需要1个菜单就够了。

1
2
3
4
5
chrome.contextMenus.create({
type: 'normal',
title: '批量导出',
id:'menu-1'
});

我们需要实现一个批量导出页面上所有的图片的功能,所以需要操作dom,根据前面说的,我们需要消息机制在内容脚本(content_scripts)中获取图片元素的地址,然后再交给扩展页面来下载。

右键菜单的点击事件,需要通过chrome.contextMenus.onClicked来实现。

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
//src/main.js 
//接收扩展页面的请求,获取图片元素返回
chrome.runtime.onMessage.addListener(function(message, sender,sendResponse) {
if (message.type == 'images') {
var imgs = document.querySelectorAll('img');
var srcs = Array.from(imgs).map(img => img.src)
sendResponse(srcs);
}
});

// 之前的dragend事件
document.addEventListener('dragend', async function(e){
if(e.target.tagName == 'IMG'){
//发生消息,从content_scripts发送到扩展页面
await chrome.runtime.sendMessage({type:'down', data:e.target.src});
}
})
//src/service_worker.js
function download(url){
var options = {
url:url
}
chrome.downloads.download(options)
}

//通过消息机制获取页面上的image元素
async function onMenuClick(){
//获取当前打开的tab
const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
//发送消息,告诉页面,我们需要获取图片元素
var response = await chrome.tabs.sendMessage(tab.id, {type:'images'});
//循环下载
(response||[]).map(download)
}

/**
* 添加右键菜单
*/
chrome.contextMenus.create({
type: 'normal',
title: '批量导出',
contexts:['all'],
id:'menu-1'
});

/**
* 右键菜单点击事件
*/
chrome.contextMenus.onClicked.addListener(function(data){
if(data.menuItemId == 'menu-1'){
onMenuClick(data)
}
})

/**
* 消息接收机制
*/
chrome.runtime.onMessage.addListener(function(message, sender,sendResponse) {
if (message.type == 'down') {
download(message.data)
}
});

这样就完成了右键批量下载的功能。

个性化配置

如果这个页面中有些图片不是我们想要的, 这个时候我们就可以通过配置页面来,实现插件的个性化配置。

manifest.json中可以通过配置options page属性,为插件指定一个配置页面。

当用户在插件图标上点击右键,选择菜单中的“选项”后,就会打开这个页面。

配置的数据存在哪呢?

chrome的插件机制提供了存储相关的api, chrome.storage可以实现在插件中数据共享。

一般有三种模式:

  • chrome.storage.local : 数据存储在本地,在删除扩展时会被清除。配额限制约为 5 MB,但可以通过请求权限来增加”unlimitedStorage”。
  • chrome.storage.sync : 如果启用同步,数据将同步到用户登录的任何 Chrome 浏览器。如果禁用,它的行为类似于storage.local.
  • chr``ome.storage.session : 在浏览器会话期间将数据保存在内存中。默认情况下,它不会暴露给内容脚本,但可以通过设置更改此行为chrome.storage.session.setAccessLevel()。配额限制约为 10 MB。

这里需要在mainfest.jsonpermissions配置: ["storage"]

添加配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
...,
"options_ui": {
"page": "./src/options.html",
"open_in_tab": false
},
...
}

// 上面配置页面,是使用窗口ui的模式打开配置页面。
// 或者使用下面的配置方式: 在新的tab里打开配置页面。

{
...,
"options_page":"./src/options.html"
...
}

这里实我集成了bootstrap ui 框架,

扩展页面只能通过script标签外链的形式引入脚本

src/options.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="./vendor/bootstrap/bootstrap.min.css">
<script src="./vendor/bootstrap/bootstrap.min.js"></script>
<script src="./options.js"></script>
</head>
<body>
<div class="mb-3">
<label for="basic-url" class="form-label">按域名过滤</label>
<div class="input-group">
<span class="input-group-text">domian:</span>
<input id="filter-url" type="text" class="form-control">
</div>
<div class="form-text">只下载匹配该domain的图片</div>
</div>
</body>
</html>

然后再新建一个src/options.js文件,添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
window.onload = function(){
//定义存储key
const FILTER_KEY='filterUrl';

//保存用户配置
function saveOptions(value){
chrome.storage.local.set(value)
}

//监听输入框
document.getElementById('filter-url').addEventListener('change',function(e){
saveOptions({[FILTER_KEY]:e.target.value||''})
})

//加在默认数据
chrome.storage.local.get([FILTER_KEY]).then((result) => {
var value = result[FILTER_KEY];
document.getElementById('filter-url').value=value||''
});

}

接着,修改src/service_worker.js批量下载的方法。实现根据配置信息来过滤下载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//通过消息机制获取页面上的image元素
async function onMenuClick() {
//获取当前打开的tab
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
//发送消息,告诉页面,我们需要获取图片元素
var response = await chrome.tabs.sendMessage(tab.id, { type: 'images' });
var data=response || []
//获取配置信息
chrome.storage.local.get(['filterUrl']).then((result) => {
var value = result['filterUrl'];
if (value) {
//循环下载
data.filter(src => src.indexOf(value) != -1).map(download)
} else {
data.map(download)
}
});
}

添加 badge

插件可以选择显示一个徽章,一个叠加在图标上的文本。 徽章可以很容易地更新浏览器操作以显示有关扩展状态的少量信息。

使用方法比较简单:

  • chrome.action.setBadgeText 设置徽章文本
  • chrome.action.setBadgeBackgroundColor 设置徽章背景色

这个api只能在扩展页面上使用,所以我们需要再内容脚本(content_scripts)中获取图片数量后,通过消息机制发送到service_worker.js中,然后调用api显示:

1
2
3
4
5
6
//src/main.js 添加一下代码

window.addEventListener('load',async function(e){
var imgs = document.querySelectorAll('img');
await chrome.runtime.sendMessage({type: 'badge', data: imgs.length + ''});
})

由于badge text只能是 string类型,所以需要将 number 类型转成 string 类型;

然后再 src/service_worker.js 添加显示徽章的方法:

1
2
3
4
5
6
7
8
9
10
chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
if (message.type == 'down') {
download(message.data)
} else if(message.type == 'badge'){
chrome.action.setBadgeBackgroundColor({color:'#f00'})
chrome.action.setBadgeText({
text: message.data
})
}
});

更新一下 manifest.json

1
2
3
4
5
6
7
8
9
10
11
{
...,
"action": {
"default_icon": {
"16": "icon16.png"
},
"default_title": "weirdo-message",
"default_popup": "./src/options.html"
}
...
}

Chrome 插件开发流程
http://example.com/2024/04/30/09/
作者
weirdo
发布于
2024年4月30日
更新于
2024年4月30日
许可协议